fix: prevent staking dashboard hang when exposure flag not cached

isPagedExposuresUsed() called storageCache.getEntry() which suspends
forever if the entry doesn't exist. The flag is only written by
ValidatorExposureUpdater (staking detail flow), so the dashboard
would hang indefinitely waiting for it.

Check cache first with isFullKeyInCache() and default to paged
exposures when the flag is absent. Also remove debug log statements.
This commit is contained in:
2026-02-27 13:41:26 +03:00
parent 2d0d46688e
commit 894e5dac22
5 changed files with 10 additions and 21 deletions
@@ -1,7 +1,5 @@
package io.novafoundation.nova.common.data.memory
import android.util.Log
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.common.utils.inBackground
import io.novafoundation.nova.common.utils.invokeOnCompletion
@@ -75,17 +73,12 @@ internal class RealComputationalCache : ComputationalCache, CoroutineScope by Co
val awaitable = mutex.withLock {
val existing = memory[key]
if (existing != null && existing.aggregateScope.isActive) {
Log.d(LOG_TAG, "Key $key requested - already present")
existing.dependents += scope
existing.awaitable
} else {
if (existing != null) {
Log.d(LOG_TAG, "Key $key requested - stale (aggregateScope cancelled), recreating")
memory.remove(key)
} else {
Log.d(LOG_TAG, "Key $key requested - creating new operation")
}
val aggregateScope = CoroutineScope(Dispatchers.Default)
@@ -104,13 +97,9 @@ internal class RealComputationalCache : ComputationalCache, CoroutineScope by Co
entry.dependents -= scope
if (entry.dependents.isEmpty()) {
Log.d(this@RealComputationalCache.LOG_TAG, "Key $key - last scope cancelled")
memory.remove(key)
entry.aggregateScope.cancel()
} else {
Log.d(this@RealComputationalCache.LOG_TAG, "Key $key - scope cancelled, ${entry.dependents.size} remaining")
}
}
}
@@ -156,7 +156,6 @@ class PezkuwiDashboardRepository(
}
kycModule.storage("KycStatuses").query(accountId, binding = { decoded ->
val enumName = decoded?.castToDictEnum()?.name
Log.d("PezkuwiDashboard", "KYC status raw enum: '$enumName' (decoded=$decoded)")
when (enumName) {
"PendingReferral" -> CitizenshipStatus.PENDING_REFERRAL
"ReferrerApproved" -> CitizenshipStatus.REFERRER_APPROVED
@@ -130,7 +130,6 @@ class RealStakingDashboardUpdateSystem(
val stats = runCatching {
stakingStatsDataSource.fetchStakingStats(stakingAccounts, stakingChains)
}.getOrElse {
Log.d("StakingDashboardUpdateSystem", "Failed to fetch staking stats after retries", it)
emptyMap()
}
@@ -405,6 +405,16 @@ class StakingRepositoryImpl(
}
private suspend fun isPagedExposuresUsed(chainId: ChainId): Boolean {
// Check if the flag is already in cache before calling getEntry, which suspends
// forever if the entry doesn't exist. The flag is written by ValidatorExposureUpdater
// which only runs in the staking detail flow, not the dashboard.
val isCached = storageCache.isFullKeyInCache(ValidatorExposureUpdater.STORAGE_KEY_PAGED_EXPOSURES, chainId)
if (!isCached) {
// Default to paged exposures (modern chains). If paged returns empty,
// getElectedValidatorsExposure will return an empty map gracefully.
return true
}
val isPagedExposuresValue = storageCache.getEntry(ValidatorExposureUpdater.STORAGE_KEY_PAGED_EXPOSURES, chainId)
return ValidatorExposureUpdater.decodeIsPagedExposuresValue(isPagedExposuresValue.content)
@@ -225,14 +225,6 @@ class RealStakingDashboardInteractor(
noPriceStakingDashboard: NoPriceStakingDashboard,
assets: Map<FullChainAssetId, Asset>,
): ExtendedLoadingState<StakingDashboard> {
Log.d(
"StakingDashboard",
"addPricesToDashboard: hasStake=${noPriceStakingDashboard.hasStake.size}, " +
"noStake=${noPriceStakingDashboard.noStake.size}, " +
"notYetResolved=${noPriceStakingDashboard.notYetResolved.size}, " +
"assets=${assets.size}"
)
val hasStakeOptions = noPriceStakingDashboard.hasStake.mapNotNull { addPriceToHasStakeItem(it, assets) }
val noStakeOptions = noPriceStakingDashboard.noStake.mapNotNull { addAssetInfoToNoStakeItem(it, assets) }
val notYetResolvedOptions = noPriceStakingDashboard.notYetResolved.mapNotNull { addAssetInfoToNotYetResolvedItem(it, assets) }