fix(simkl): deduplicate startup activity checks

This commit is contained in:
tapframe 2026-07-23 14:59:51 +05:30
parent 1906518322
commit 96d0a31f45
8 changed files with 73 additions and 10 deletions

View file

@ -102,8 +102,8 @@ object LibraryRepository {
activeLibraryProvider()?.let { provider ->
refreshLibraryProvider(
provider = provider,
reason = "authentication change",
intent = TrackingRefreshIntent.INVALIDATED,
reason = "connection state change",
intent = provider.connectionRefreshIntent,
)
}
publish()

View file

@ -170,9 +170,6 @@ object SimklProgressRepository {
}
}
internal fun simklProgressRefreshIntent(sourceChanged: Boolean): TrackingRefreshIntent =
if (sourceChanged) TrackingRefreshIntent.INVALIDATED else TrackingRefreshIntent.AUTOMATIC
object SimklTrackingProgressProvider : TrackingProgressProvider {
override val providerId: TrackingProviderId = TrackingProviderId.SIMKL
override val changes: Flow<Unit> = SimklProgressRepository.uiState.map { Unit }
@ -182,7 +179,7 @@ object SimklTrackingProgressProvider : TrackingProgressProvider {
override fun onProfileChanged() = SimklProgressRepository.ensureLoaded()
override suspend fun refresh(force: Boolean, sourceChanged: Boolean) =
SimklProgressRepository.refresh(simklProgressRefreshIntent(sourceChanged))
SimklProgressRepository.refresh(simklProgressRefreshIntent)
override fun snapshot(): TrackingProgressSnapshot {
val state = SimklProgressRepository.uiState.value

View file

@ -166,6 +166,7 @@ object SimklLibraryRepository {
object SimklTrackingLibraryProvider : TrackingLibraryProvider {
override val providerId: TrackingProviderId = TrackingProviderId.SIMKL
override val changes: Flow<Unit> = SimklLibraryRepository.uiState.map { Unit }
override val connectionRefreshIntent: TrackingRefreshIntent = simklConnectionRefreshIntent
override fun ensureLoaded() = SimklLibraryRepository.ensureLoaded()

View file

@ -8,6 +8,8 @@ import kotlinx.coroutines.sync.withLock
internal const val SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES = 15
internal const val SIMKL_AUTOMATIC_REFRESH_INTERVAL_MS =
SIMKL_AUTOMATIC_REFRESH_INTERVAL_MINUTES * 60L * 1_000L
internal val simklConnectionRefreshIntent = TrackingRefreshIntent.AUTOMATIC
internal val simklProgressRefreshIntent = TrackingRefreshIntent.AUTOMATIC
internal fun shouldRunSimklRefresh(
intent: TrackingRefreshIntent,

View file

@ -79,6 +79,7 @@ enum class TrackingRefreshIntent {
interface TrackingLibraryProvider {
val providerId: TrackingProviderId
val changes: Flow<Unit>
val connectionRefreshIntent: TrackingRefreshIntent
fun ensureLoaded()
fun prepare() = Unit

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.map
object TraktTrackingLibraryProvider : TrackingLibraryProvider {
override val providerId: TrackingProviderId = TrackingProviderId.TRAKT
override val changes: Flow<Unit> = TraktLibraryRepository.uiState.map { Unit }
override val connectionRefreshIntent: TrackingRefreshIntent = TrackingRefreshIntent.INVALIDATED
override fun ensureLoaded() = TraktLibraryRepository.ensureLoaded()

View file

@ -11,14 +11,14 @@ import kotlin.test.assertTrue
class SimklRefreshPolicyTest {
@Test
fun `progress refresh keeps provider policy unless the source changes`() {
fun `startup refresh paths share automatic freshness`() {
assertEquals(
TrackingRefreshIntent.AUTOMATIC,
simklProgressRefreshIntent(sourceChanged = false),
simklConnectionRefreshIntent,
)
assertEquals(
TrackingRefreshIntent.INVALIDATED,
simklProgressRefreshIntent(sourceChanged = true),
TrackingRefreshIntent.AUTOMATIC,
simklProgressRefreshIntent,
)
}
@ -119,6 +119,58 @@ class SimklRefreshPolicyTest {
assertEquals(1, executions)
}
@Test
fun `sequential startup refresh paths execute only once`() = runBlocking {
val gate = SimklRefreshGate()
var lastCheckedAtEpochMs: Long? = null
var executions = 0
listOf(simklConnectionRefreshIntent, simklProgressRefreshIntent).forEach { intent ->
gate.runIfNeeded(
profileGeneration = 7L,
shouldRun = {
shouldRunSimklRefresh(
intent = intent,
lastCheckedAtEpochMs = lastCheckedAtEpochMs,
nowEpochMs = 1_000L,
hasError = false,
)
},
) {
executions += 1
lastCheckedAtEpochMs = 1_000L
}
}
assertEquals(1, executions)
}
@Test
fun `mutation invalidation still refreshes after startup check`() = runBlocking {
val gate = SimklRefreshGate()
var lastCheckedAtEpochMs: Long? = null
var executions = 0
listOf(simklConnectionRefreshIntent, TrackingRefreshIntent.INVALIDATED).forEach { intent ->
gate.runIfNeeded(
profileGeneration = 7L,
shouldRun = {
shouldRunSimklRefresh(
intent = intent,
lastCheckedAtEpochMs = lastCheckedAtEpochMs,
nowEpochMs = 1_000L,
hasError = false,
)
},
) {
executions += 1
lastCheckedAtEpochMs = 1_000L
}
}
assertEquals(2, executions)
}
@Test
fun `a new profile generation is not coalesced with an old profile refresh`() = runBlocking {
val gate = SimklRefreshGate()

View file

@ -1,9 +1,18 @@
package com.nuvio.app.features.trakt
import com.nuvio.app.features.tracking.TrackingRefreshIntent
import kotlin.test.Test
import kotlin.test.assertEquals
class TraktTrackingLibraryProviderTest {
@Test
fun `connection events preserve forced Trakt refreshes`() {
assertEquals(
TrackingRefreshIntent.INVALIDATED,
TraktTrackingLibraryProvider.connectionRefreshIntent,
)
}
@Test
fun `default toggle changes only watchlist membership`() {
val membership = mapOf(