Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-04 11:58:10 +03:00
parent 563d0834c4
commit 26a6da1b85
4 changed files with 174 additions and 27 deletions

View file

@ -13,13 +13,8 @@ import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.io.File
@ -67,6 +62,8 @@ internal class DefaultNetworksStatusesStore(
override fun get(userWalletId: UserWalletId): Flow<Set<SimpleNetworkStatus>> {
return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] }
.adaptiveThrottle()
.conflate()
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? {
@ -180,4 +177,61 @@ internal class DefaultNetworksStatusesStore(
}
}
}
}
@Suppress("MagicNumber")
internal fun <T> Flow<Set<T>>.adaptiveThrottle(): Flow<Set<T>> = channelFlow {
var accumulator: Set<T>? = null
var lastEmitTime = 0L
// params that control maximum emissions that can be throttled
var densityLevel = 0
val maxDensity = 10
// params that control maximum delay and growth of delay between emissions
var lastDelay = 0L
val maxDelay = 1500L
val growthFactor = 250L
fun resetThrottling() {
lastDelay = 0L
densityLevel = 0
}
this@adaptiveThrottle.collectLatest { newSet ->
val previousSet: Collection<T>? = accumulator
accumulator = newSet
when {
// first value, just emit
previousSet == null -> resetThrottling()
// changed size, just emit
previousSet.size != newSet.size -> resetThrottling()
// apply adaptive throttling
else -> {
val networksCount = newSet.size
// more networks - more throttling
val cooldownThreshold = when {
networksCount in 10..25 -> 300L
networksCount > 25 -> 500L
// 0..9 networks
else -> 100L
}
val now = System.currentTimeMillis()
val timeSinceLastEmit = now - lastEmitTime
if (timeSinceLastEmit < cooldownThreshold && densityLevel < maxDensity) {
lastDelay = (lastDelay + growthFactor).coerceAtMost(maximumValue = maxDelay)
densityLevel += 1
delay(lastDelay)
} else {
resetThrottling()
}
}
}
lastEmitTime = System.currentTimeMillis()
channel.send(newSet)
}
}

View file

@ -0,0 +1,109 @@
package com.tangem.data.networks.store
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class StoreAdaptiveThrottleTest {
@Test
fun `first value is emitted immediately`() = runTest {
val flow = flowOf(setOf(1, 2, 3)).adaptiveThrottle()
flow.test {
val item = awaitItem()
assertThat(item).isEqualTo(setOf(1, 2, 3))
awaitComplete()
}
}
@Test
fun `size change bypasses throttling`() = runTest {
val upstream = MutableSharedFlow<Set<Int>>()
upstream.adaptiveThrottle().test {
upstream.emit(setOf(1, 2))
assertThat(awaitItem()).isEqualTo(setOf(1, 2))
upstream.emit(setOf(1, 2, 3))
assertThat(awaitItem()).isEqualTo(setOf(1, 2, 3))
upstream.emit(setOf(1))
assertThat(awaitItem()).isEqualTo(setOf(1))
}
}
@Test
fun `same size events trigger throttling delay`() = runTest {
val upstream = MutableSharedFlow<Set<Int>>()
upstream.adaptiveThrottle().test {
upstream.emit(setOf(1, 2))
awaitItem()
upstream.emit(setOf(3, 4))
// delay should happen
expectNoEvents()
advanceTimeBy(250)
val item = awaitItem()
assertThat(item).isEqualTo(setOf(3, 4))
}
}
@Test
fun `rapid events result in only latest emission due to collectLatest`() = runTest {
val upstream = MutableSharedFlow<Set<Int>>()
upstream.adaptiveThrottle().test {
upstream.emit(setOf(1, 2))
awaitItem()
launch {
upstream.emit(setOf(3, 4))
upstream.emit(setOf(5, 6))
upstream.emit(setOf(7, 8))
}
// delay should happen
expectNoEvents()
advanceTimeBy(250)
val item = awaitItem()
assertThat(item).isEqualTo(setOf(7, 8))
}
}
@Test
fun `throttling resets when cooldown window passed`() = runTest {
val upstream = MutableSharedFlow<Set<Int>>()
upstream.adaptiveThrottle().test {
upstream.emit(setOf(1, 2))
awaitItem()
upstream.emit(setOf(3, 4))
expectNoEvents()
advanceTimeBy(250)
awaitItem()
// wait long enough to reset throttling
advanceTimeBy(2000)
upstream.emit(setOf(5, 6))
val item = awaitItem()
assertThat(item).isEqualTo(setOf(5, 6))
}
}
}