fix: clear the remaining compiler warnings in amethyst and desktopApp

Second half of the warning sweep, covering :amethyst (both flavors, all
build types, unit + instrumented tests), :desktopApp and :benchmark:

- GitReplyEvent (NIP-34 kind 1622) is deprecated in favour of NIP-22
  comments, but events already on relays still arrive and still have to be
  routed, rendered and surfaced. @Suppress("DEPRECATION") with that reason
  at the five production sites and the coverage test that pins them.
- The four instrumented Compose tests move to
  androidx.compose.ui.test.junit4.v2.createComposeRule. The v2 factory
  returns the same ComposeContentTestRule, so mainClock, setContent and the
  node assertions are unchanged; only the effect dispatcher differs.
- RelayAuthPromptBusTest / RelayAuthSessionGrantsTest: @OptIn for the
  ExperimentalCoroutinesApi members (testScheduler.currentTime, runCurrent)
  they already use, matching the annotation the file's other tests carry.
- MarmotFileUploader: drop a nullable alias of a non-null cipher, left
  behind when the v2 reference stopped being conditional.
- LocalCacheSearchParityTest: hoist the Json format out of the loader.
- LivesSection: FlowRowOverflow and FlowRow's overflow parameter are
  deprecated; the non-deprecated overload already clips beyond maxLines.
- HexBenchmark: drop a bare `null` expression statement from the measured
  lambda.

Also fixes :commons:compileCommonMainKotlinMetadata, which did not compile
at all: shared code called BigDecimal.toLong(), which resolves in every
platform compilation (every actual is a Number) but not in the common
metadata one, where only the expect class's own members are visible.
`expect class BigDecimal : Number` cannot work — java.math.BigDecimal leaves
toByte()/toShort() abstract, so the JVM typealias fails the expect/actual
modality check — so the conversion is a top-level expect/actual extension
instead, with actuals next to each BigDecimal actual.

Verified warning- and error-free across the jvm, android (play/fdroid ×
debug/release/benchmark), linuxX64, and the common/jvmAndroid/native/apple/
ios metadata compilations. The apple actuals are checked by
compileAppleMainKotlinMetadata, which runs the frontend against the Apple
klibs without needing a macOS host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123kXtseu4X18hL3GMDcdER
This commit is contained in:
Claude
2026-09-12 16:17:00 +00:00
parent a4b83b86e7
commit d63e14bb36
23 changed files with 138 additions and 33 deletions
@@ -31,7 +31,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.getUnclippedBoundsInRoot import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -62,7 +62,8 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PlaybackErrorOverlayFitTest { class PlaybackErrorOverlayFitTest {
@get:Rule val rule = createComposeRule() @get:Rule
val rule = createComposeRule()
private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
@@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.service.playback.composable.audioSquare import com.vitorpamplona.amethyst.service.playback.composable.audioSquare
@@ -50,7 +50,8 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AudioPlayerBoxOverflowTest { class AudioPlayerBoxOverflowTest {
@get:Rule val rule = createComposeRule() @get:Rule
val rule = createComposeRule()
private class Bounds { private class Bounds {
var top = 0f var top = 0f
@@ -31,7 +31,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
@@ -70,7 +70,8 @@ import org.junit.Test
* fallback would silently start reading a dead value too. * fallback would silently start reading a dead value too.
*/ */
class ComposeImeInsetWedgeTest { class ComposeImeInsetWedgeTest {
@get:Rule val rule = createComposeRule() @get:Rule
val rule = createComposeRule()
private val keyboardHeight = 957 private val keyboardHeight = 957
@@ -27,7 +27,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.ui.actions.DeferredCrossfade import com.vitorpamplona.amethyst.ui.actions.DeferredCrossfade
@@ -201,6 +201,9 @@ class EventNotificationConsumer(
.onFailure { Log.d(TAG) { "Skipping non-decodable npub $npub: ${it.message}" } } .onFailure { Log.d(TAG) { "Skipping non-decodable npub $npub: ${it.message}" } }
.getOrNull() .getOrNull()
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
// events already on relays still arrive and still have to be routed.
@Suppress("DEPRECATION")
private suspend fun dispatchForAccount( private suspend fun dispatchForAccount(
event: Event, event: Event,
account: Account, account: Account,
@@ -112,6 +112,9 @@ class NotificationDispatcher(
// recipient account. // recipient account.
// `internal` (was `private`) so the notification-kinds contract test // `internal` (was `private`) so the notification-kinds contract test
// can pin the push-side kind set against the in-app feed's kind set. // can pin the push-side kind set against the in-app feed's kind set.
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
// events already on relays still arrive and still have to be routed.
@Suppress("DEPRECATION")
internal val NOTIFICATION_KINDS: Set<Int> = internal val NOTIFICATION_KINDS: Set<Int> =
setOf( setOf(
// Direct-arrival // Direct-arrival
@@ -81,6 +81,9 @@ object CodeNotification {
event: GitPullRequestUpdateEvent, event: GitPullRequestUpdateEvent,
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_pr_update, event.content) ) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_pr_update, event.content)
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
// events already on relays still arrive and still have to be rendered.
@Suppress("DEPRECATION")
suspend fun notify( suspend fun notify(
context: Context, context: Context,
account: Account, account: Account,
@@ -119,7 +119,6 @@ class MarmotFileUploader(
// imprecisely. // imprecisely.
val canonicalMediaType = MarmotMediaType.canonicalize(mimeType) ?: GENERIC_MEDIA_TYPE val canonicalMediaType = MarmotMediaType.canonicalize(mimeType) ?: GENERIC_MEDIA_TYPE
val cipher = EncryptedMediaV2Cipher(exporterSecret, canonicalMediaType, filename) val cipher = EncryptedMediaV2Cipher(exporterSecret, canonicalMediaType, filename)
val v2Cipher = cipher
item.orchestrator.uploadEncrypted( item.orchestrator.uploadEncrypted(
uri = media.uri, uri = media.uri,
@@ -142,21 +141,19 @@ class MarmotFileUploader(
// compression and metadata stripping — because that is what the // compression and metadata stripping — because that is what the
// key was derived from. // key was derived from.
val reference = val reference =
v2Cipher?.let {
EncryptedMediaReferenceV2( EncryptedMediaReferenceV2(
locators = locators =
listOf( listOf(
MediaLocatorV2(EncryptedMediaPolicyV2.INITIAL_LOCATOR_KIND, serverResult.url), MediaLocatorV2(EncryptedMediaPolicyV2.INITIAL_LOCATOR_KIND, serverResult.url),
), ),
ciphertextSha256 = it.ciphertextSha256, ciphertextSha256 = cipher.ciphertextSha256,
plaintextSha256 = it.plaintextSha256, plaintextSha256 = cipher.plaintextSha256,
nonce = it.nonce, nonce = cipher.nonce,
mediaType = it.mediaType, mediaType = cipher.mediaType,
filename = filename, filename = filename,
dim = serverResult.fileHeader.dim?.toString(), dim = serverResult.fileHeader.dim?.toString(),
thumbhash = serverResult.fileHeader.thumbHash?.thumbhash, thumbhash = serverResult.fileHeader.thumbHash?.thumbhash,
) )
}
results.add( results.add(
Mip04UploadResult( Mip04UploadResult(
url = serverResult.url, url = serverResult.url,
@@ -139,6 +139,9 @@ class NotificationFeedFilter(
AttestationRequestEvent.KIND, AttestationRequestEvent.KIND,
) )
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
// events already on relays still arrive and still have to be routed.
@Suppress("DEPRECATION")
val NOTIFICATION_KINDS = val NOTIFICATION_KINDS =
// Kinds that RENDER as a row on the Notifications tab. This is a // Kinds that RENDER as a row on the Notifications tab. This is a
// display gate over whatever is already in LocalCache — it plays no // display gate over whatever is already in LocalCache — it plays no
@@ -268,6 +271,9 @@ class NotificationFeedFilter(
// Shared with EventNotificationConsumer so push notifications and the // Shared with EventNotificationConsumer so push notifications and the
// in-app feed apply the same per-kind "is this event for me" rule. // in-app feed apply the same per-kind "is this event for me" rule.
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
// events already on relays still arrive and still have to be routed.
@Suppress("DEPRECATION")
fun tagsAnEventByUser( fun tagsAnEventByUser(
note: Note, note: Note,
authorHex: HexKey, authorHex: HexKey,
@@ -47,6 +47,9 @@ import java.io.File
*/ */
class LocalCacheSearchParityTest { class LocalCacheSearchParityTest {
companion object { companion object {
/** Hoisted out of [loadCorpus]: building a Json format is expensive enough that the compiler warns on it. */
private val json = Json { ignoreUnknownKeys = true }
private lateinit var corpus: List<Event> private lateinit var corpus: List<Event>
@BeforeClass @BeforeClass
@@ -57,7 +60,7 @@ class LocalCacheSearchParityTest {
.firstOrNull { it.isFile } .firstOrNull { it.isFile }
?: error("tools/search-parity/fixture.json is missing; run tools/search-parity/fetch_fixtures.py") ?: error("tools/search-parity/fixture.json is missing; run tools/search-parity/fetch_fixtures.py")
val root = Json { ignoreUnknownKeys = true }.parseToJsonElement(file.readText()).jsonObject val root = json.parseToJsonElement(file.readText()).jsonObject
corpus = corpus =
root["cases"]!! root["cases"]!!
.jsonArray .jsonArray
@@ -153,6 +153,7 @@ class RelayAuthPromptBusTest {
* answer already sitting in the deferred, so the relay it belongs to goes unauthenticated for that * answer already sitting in the deferred, so the relay it belongs to goes unauthenticated for that
* long despite the user having answered. Marking it shown is what makes the answer land now. * long despite the user having answered. Marking it shown is what makes the answer land now.
*/ */
@OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun anAnswerFannedOutToAQueuedPromptLandsWithoutWaitingOutTheQueueWindow() = fun anAnswerFannedOutToAQueuedPromptLandsWithoutWaitingOutTheQueueWindow() =
runTest { runTest {
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPermissionStore
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthVerdict import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthVerdict
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -185,6 +186,7 @@ class RelayAuthSessionGrantsTest {
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun promotingAGrantToAlwaysNeverOpensAGapThatRePrompts() = fun promotingAGrantToAlwaysNeverOpensAGapThatRePrompts() =
runTest { runTest {
@@ -205,6 +207,7 @@ class RelayAuthSessionGrantsTest {
assertEquals(RelayAuthVerdict.ALLOW, ledger.decide(askable(relay))) assertEquals(RelayAuthVerdict.ALLOW, ledger.decide(askable(relay)))
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun neverAllowStopsAuthenticatingBeforeItsWriteLands() = fun neverAllowStopsAuthenticatingBeforeItsWriteLands() =
runTest { runTest {
@@ -70,6 +70,7 @@ class Nip34NotificationCoverageTest {
* A NIP-22 [com.vitorpamplona.quartz.nip22Comments.CommentEvent] handles * A NIP-22 [com.vitorpamplona.quartz.nip22Comments.CommentEvent] handles
* modern comments through its own separate wiring. * modern comments through its own separate wiring.
*/ */
@Suppress("DEPRECATION")
private val nip34ParticipantKinds = private val nip34ParticipantKinds =
setOf( setOf(
GitPatchEvent.KIND, GitPatchEvent.KIND,
@@ -131,6 +132,7 @@ class Nip34NotificationCoverageTest {
* uppercase `E`. Asserting the wrong half passes the kind list while matching * uppercase `E`. Asserting the wrong half passes the kind list while matching
* nothing on the wire. * nothing on the wire.
*/ */
@Suppress("DEPRECATION")
@Test @Test
fun `status kinds are pulled by the lowercase-e engagement subscription`() { fun `status kinds are pulled by the lowercase-e engagement subscription`() {
val eAnchoredActivityKinds = val eAnchoredActivityKinds =
@@ -136,7 +136,7 @@ class HexBenchmark {
/** The pre-existing two-pass way to safely decode an id, for comparison with [hexDecode64OrNull]. */ /** The pre-existing two-pass way to safely decode an id, for comparison with [hexDecode64OrNull]. */
@Test @Test
fun hexIsHex64ThenDecode() { fun hexIsHex64ThenDecode() {
r.measureRepeated { if (Hex.isHex64(hex)) Hex.decode(hex) else null } r.measureRepeated { if (Hex.isHex64(hex)) Hex.decode(hex) }
} }
@Test @Test
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.utils.toLongValue
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -162,7 +163,7 @@ class LiveStreamTopZappersViewModel(
when (val ev = note.event) { when (val ev = note.event) {
is LnZapEvent -> { is LnZapEvent -> {
val request = ev.zapRequest ?: return null val request = ev.zapRequest ?: return null
val sats = ev.amount()?.toLong() ?: return null val sats = ev.amount()?.toLongValue() ?: return null
ZapContribution(note.idHex, request.pubKey, request.isAnonTagged(), sats) ZapContribution(note.idHex, request.pubKey, request.isAnonTagged(), sats)
} }
is Bolt12ZapEvent -> { is Bolt12ZapEvent -> {
@@ -178,7 +179,7 @@ class LiveStreamTopZappersViewModel(
): ZapContribution? { ): ZapContribution? {
val receiptEv = receiptNote?.event as? LnZapEvent ?: return null val receiptEv = receiptNote?.event as? LnZapEvent ?: return null
val request = zapRequestNote.event as? LnZapRequestEvent ?: return null val request = zapRequestNote.event as? LnZapRequestEvent ?: return null
val sats = receiptEv.amount()?.toLong() ?: return null val sats = receiptEv.amount()?.toLongValue() ?: return null
return ZapContribution(receiptNote.idHex, request.pubKey, request.isAnonTagged(), sats) return ZapContribution(receiptNote.idHex, request.pubKey, request.isAnonTagged(), sats)
} }
} }
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.viewmodels
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.utils.toLongValue
/** /**
* One in-flight kind-9735 zap to render as a floating overlay on the * One in-flight kind-9735 zap to render as a floating overlay on the
@@ -59,7 +60,7 @@ data class RoomZap(
eventId = event.id, eventId = event.id,
sourcePubkey = event.zapRequest?.pubKey ?: event.pubKey, sourcePubkey = event.zapRequest?.pubKey ?: event.pubKey,
targetPubkey = event.zappedAuthor().firstOrNull(), targetPubkey = event.zappedAuthor().firstOrNull(),
amountSats = event.amount?.toLong(), amountSats = event.amount?.toLongValue(),
createdAtSec = event.createdAt, createdAtSec = event.createdAt,
) )
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowOverflow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
@@ -146,7 +145,6 @@ fun LivesSection(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
maxLines = 2, maxLines = 2,
overflow = FlowRowOverflow.Clip,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
ranked.forEach { channel -> ranked.forEach { channel ->
@@ -0,0 +1,23 @@
/*
* 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.utils
actual fun BigDecimal.toLongValue(): Long = toLong()
@@ -760,7 +760,7 @@ class MarmotInboundProcessor(
val author = payloadAuthor(candidate.content.decodeToString()) val author = payloadAuthor(candidate.content.decodeToString())
val sender = candidate.senderAccount val sender = candidate.senderAccount
val valid = author != null && sender != null && author == sender val valid = author != null && sender != null && author == sender
if (valid && sender != null) { if (valid) {
convergence.recordWitness(groupId, candidate.stateId, sender) convergence.recordWitness(groupId, candidate.stateId, sender)
} }
return GroupEventResult.AppMessageOnCandidateBranch( return GroupEventResult.AppMessageOnCandidateBranch(
@@ -561,7 +561,7 @@ class MarmotConvergenceEngine(
return mutex.withLock { return mutex.withLock {
val ctx = contexts[groupId] ?: return@withLock null val ctx = contexts[groupId] ?: return@withLock null
if (rewound && selectedTipId != null) { if (rewound) {
adoptBranch(ctx, graph, selectedTipId, inputs.baseId) adoptBranch(ctx, graph, selectedTipId, inputs.baseId)
} }
// Keep the states of branches that LOST but stay eligible: losing // Keep the states of branches that LOST but stay eligible: losing
@@ -576,7 +576,7 @@ class MarmotConvergenceEngine(
.forEach { tipId -> .forEach { tipId ->
var cursor: String? = tipId var cursor: String? = tipId
while (cursor != null && cursor !in canonical) { while (cursor != null && cursor !in canonical) {
graph.statesById[cursor]?.let { ctx.candidateStates[cursor!!] = it } graph.statesById[cursor]?.let { ctx.candidateStates[cursor] = it }
cursor = graph.parentOf[cursor] cursor = graph.parentOf[cursor]
} }
} }
@@ -23,3 +23,15 @@ package com.vitorpamplona.quartz.utils
operator fun BigDecimal.plus(other: BigDecimal): BigDecimal = add(other) operator fun BigDecimal.plus(other: BigDecimal): BigDecimal = add(other)
operator fun BigDecimal.minus(other: BigDecimal): BigDecimal = subtract(other) operator fun BigDecimal.minus(other: BigDecimal): BigDecimal = subtract(other)
/**
* Truncate to a Long, the way Number.toLong() does on every platform.
*
* It has to be an expect *function* rather than a member of `expect class
* BigDecimal`: every actual is already a Number and so already has toLong(),
* but java.math.BigDecimal leaves toByte()/toShort() abstract, which makes
* `expect class BigDecimal : Number` impossible to actualize with the JVM
* typealias. Without this, `amount.toLong()` in shared code resolves only in
* the platform compilations and breaks `compileCommonMainKotlinMetadata`.
*/
expect fun BigDecimal.toLongValue(): Long
@@ -0,0 +1,23 @@
/*
* 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.utils
actual fun BigDecimal.toLongValue(): Long = toLong()
@@ -0,0 +1,23 @@
/*
* 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.utils
actual fun BigDecimal.toLongValue(): Long = toLong()