From 96d0a31f45d18e937c4c31a7240ec01969226199 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:59:51 +0530 Subject: [PATCH] fix(simkl): deduplicate startup activity checks --- .../app/features/library/LibraryRepository.kt | 4 +- .../simkl/SimklApplicationAdapters.kt | 5 +- .../app/features/simkl/SimklLibraryAdapter.kt | 1 + .../app/features/simkl/SimklRefreshPolicy.kt | 2 + .../app/features/tracking/TrackingReads.kt | 1 + .../trakt/TraktTrackingLibraryProvider.kt | 1 + .../features/simkl/SimklRefreshPolicyTest.kt | 60 +++++++++++++++++-- .../trakt/TraktTrackingLibraryProviderTest.kt | 9 +++ 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt index 44c3d896f..eb11aa9fa 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryRepository.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt index 381ba2d59..4c6c1a484 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklApplicationAdapters.kt @@ -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 = 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 diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklLibraryAdapter.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklLibraryAdapter.kt index d10b06ee7..f4b5830fe 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklLibraryAdapter.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklLibraryAdapter.kt @@ -166,6 +166,7 @@ object SimklLibraryRepository { object SimklTrackingLibraryProvider : TrackingLibraryProvider { override val providerId: TrackingProviderId = TrackingProviderId.SIMKL override val changes: Flow = SimklLibraryRepository.uiState.map { Unit } + override val connectionRefreshIntent: TrackingRefreshIntent = simklConnectionRefreshIntent override fun ensureLoaded() = SimklLibraryRepository.ensureLoaded() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicy.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicy.kt index b40b5fd58..333668b17 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicy.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicy.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingReads.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingReads.kt index 5cbdab6ab..f0c500c8e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingReads.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/tracking/TrackingReads.kt @@ -79,6 +79,7 @@ enum class TrackingRefreshIntent { interface TrackingLibraryProvider { val providerId: TrackingProviderId val changes: Flow + val connectionRefreshIntent: TrackingRefreshIntent fun ensureLoaded() fun prepare() = Unit diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProvider.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProvider.kt index 37767810d..b0670a7bd 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProvider.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProvider.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.map object TraktTrackingLibraryProvider : TrackingLibraryProvider { override val providerId: TrackingProviderId = TrackingProviderId.TRAKT override val changes: Flow = TraktLibraryRepository.uiState.map { Unit } + override val connectionRefreshIntent: TrackingRefreshIntent = TrackingRefreshIntent.INVALIDATED override fun ensureLoaded() = TraktLibraryRepository.ensureLoaded() diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicyTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicyTest.kt index 849c3f8ee..3eace5e7b 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicyTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/simkl/SimklRefreshPolicyTest.kt @@ -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() diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProviderTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProviderTest.kt index c5bf96a31..ed2f0b9bb 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProviderTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/trakt/TraktTrackingLibraryProviderTest.kt @@ -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(