mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-17 04:48:37 +00:00
fix(simkl): refresh watch state after manual sync
This commit is contained in:
parent
4f3eaee2cc
commit
9b278e6cb0
6 changed files with 181 additions and 14 deletions
|
|
@ -57,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.nuvio.app.core.ui.NuvioLoadingIndicator
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.simkl.SimklAuthError
|
||||
import com.nuvio.app.features.simkl.SimklAuthRepository
|
||||
import com.nuvio.app.features.simkl.SimklAuthUiState
|
||||
|
|
@ -64,12 +65,14 @@ import com.nuvio.app.features.simkl.SimklBrandAsset
|
|||
import com.nuvio.app.features.simkl.SimklConnectionMode
|
||||
import com.nuvio.app.features.simkl.SimklSyncRepository
|
||||
import com.nuvio.app.features.simkl.simklBrandPainter
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import com.nuvio.app.features.tracking.TrackingRefreshIntent
|
||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
import com.nuvio.app.features.trakt.TraktAuthUiState
|
||||
import com.nuvio.app.features.trakt.TraktBrandAsset
|
||||
import com.nuvio.app.features.trakt.TraktConnectionMode
|
||||
import com.nuvio.app.features.trakt.traktBrandPainter
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressSourceCoordinator
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
|
|
@ -156,6 +159,17 @@ internal fun TrackingProviderCards(
|
|||
}.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSyncInfo by rememberSaveable { mutableStateOf(false) }
|
||||
val onSimklSyncRequested: () -> Unit = {
|
||||
scope.launch {
|
||||
WatchProgressSourceCoordinator.refreshProviderAndActiveSource(
|
||||
profileId = ProfileRepository.activeProfileId,
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
refreshProvider = {
|
||||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||
val useTwoColumns = maxWidth >= 600.dp
|
||||
|
|
@ -176,11 +190,7 @@ internal fun TrackingProviderCards(
|
|||
uiState = simklUiState,
|
||||
isSyncing = syncState.isLoading,
|
||||
syncErrorMessage = syncState.errorMessage,
|
||||
onSyncRequested = {
|
||||
scope.launch {
|
||||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
}
|
||||
},
|
||||
onSyncRequested = onSimklSyncRequested,
|
||||
onInfoRequested = { showSyncInfo = true },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
|
|
@ -200,11 +210,7 @@ internal fun TrackingProviderCards(
|
|||
uiState = simklUiState,
|
||||
isSyncing = syncState.isLoading,
|
||||
syncErrorMessage = syncState.errorMessage,
|
||||
onSyncRequested = {
|
||||
scope.launch {
|
||||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
}
|
||||
},
|
||||
onSyncRequested = onSimklSyncRequested,
|
||||
onInfoRequested = { showSyncInfo = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,23 +73,23 @@ object SimklSyncRepository : TrackingProfileStore {
|
|||
scope.launch { refresh(intent, origin) }
|
||||
}
|
||||
|
||||
suspend fun refresh(intent: TrackingRefreshIntent) {
|
||||
suspend fun refresh(intent: TrackingRefreshIntent): Boolean =
|
||||
refresh(intent, SimklRefreshOrigin.MANUAL_SYNC)
|
||||
}
|
||||
|
||||
internal suspend fun refresh(
|
||||
intent: TrackingRefreshIntent,
|
||||
origin: SimklRefreshOrigin,
|
||||
) {
|
||||
): Boolean {
|
||||
ensureLoaded()
|
||||
val requestId = refreshRequestSequence.incrementAndGet()
|
||||
val requestedGeneration = profileGeneration
|
||||
val requestedProfileId = ProfileRepository.activeProfileId
|
||||
val before = _state.value
|
||||
SimklWatchDiagnostics.logRefreshRequest(
|
||||
requestId = requestId,
|
||||
origin = origin,
|
||||
intent = intent,
|
||||
profileId = ProfileRepository.activeProfileId,
|
||||
profileId = requestedProfileId,
|
||||
profileGeneration = requestedGeneration,
|
||||
authenticated = SimklAuthRepository.isAuthenticated.value,
|
||||
snapshot = before.snapshot,
|
||||
|
|
@ -132,6 +132,12 @@ object SimklSyncRepository : TrackingProfileStore {
|
|||
before = before,
|
||||
after = _state.value,
|
||||
)
|
||||
val completed = _state.value
|
||||
return requestedGeneration == profileGeneration &&
|
||||
requestedProfileId == ProfileRepository.activeProfileId &&
|
||||
SimklAuthRepository.isAuthenticated.value &&
|
||||
completed.hasLoaded &&
|
||||
completed.errorMessage == null
|
||||
}
|
||||
|
||||
private suspend fun refreshSnapshot(generation: Long) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
|
||||
internal suspend fun coordinateTrackingProviderRefresh(
|
||||
providerId: TrackingProviderId,
|
||||
refreshProvider: suspend () -> Boolean,
|
||||
activeProviderId: () -> TrackingProviderId?,
|
||||
refreshActiveReadModels: suspend () -> Boolean,
|
||||
): Boolean {
|
||||
if (!refreshProvider()) return false
|
||||
if (activeProviderId() != providerId) return true
|
||||
return refreshActiveReadModels()
|
||||
}
|
||||
|
|
@ -309,6 +309,22 @@ object WatchProgressSourceCoordinator {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun refreshProviderAndActiveSource(
|
||||
profileId: Int,
|
||||
providerId: TrackingProviderId,
|
||||
refreshProvider: suspend () -> Boolean,
|
||||
): Boolean = coordinateTrackingProviderRefresh(
|
||||
providerId = providerId,
|
||||
refreshProvider = refreshProvider,
|
||||
activeProviderId = {
|
||||
ensureSourceStateLoaded()
|
||||
currentContext(profileId).effectiveSource.providerId
|
||||
},
|
||||
refreshActiveReadModels = {
|
||||
refreshActiveSource(profileId = profileId, force = false).succeeded
|
||||
},
|
||||
)
|
||||
|
||||
private fun ensureSourceStateLoadedForGeneration(expectedGeneration: Long) {
|
||||
synchronized(startLock) {
|
||||
ensureCoordinatorGeneration(expectedGeneration)
|
||||
|
|
|
|||
|
|
@ -152,6 +152,44 @@ class SimklRefreshPolicyTest {
|
|||
assertEquals(1, executions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `manual sync followed by read model refresh executes once`() = runBlocking {
|
||||
val gate = SimklRefreshGate()
|
||||
var lastCheckedAtEpochMs: Long? = null
|
||||
var executions = 0
|
||||
|
||||
val outcomes = listOf(
|
||||
TrackingRefreshIntent.USER_INITIATED,
|
||||
TrackingRefreshIntent.AUTOMATIC,
|
||||
TrackingRefreshIntent.AUTOMATIC,
|
||||
).map { intent ->
|
||||
gate.runIfNeeded(
|
||||
profileGeneration = 7L,
|
||||
shouldRun = {
|
||||
shouldRunSimklRefresh(
|
||||
intent = intent,
|
||||
lastCheckedAtEpochMs = lastCheckedAtEpochMs,
|
||||
nowEpochMs = 1_000L,
|
||||
hasError = false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
executions += 1
|
||||
lastCheckedAtEpochMs = 1_000L
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
SimklRefreshGateOutcome.EXECUTED,
|
||||
SimklRefreshGateOutcome.FRESHNESS_SKIPPED,
|
||||
SimklRefreshGateOutcome.FRESHNESS_SKIPPED,
|
||||
),
|
||||
outcomes,
|
||||
)
|
||||
assertEquals(1, executions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mutation invalidation still refreshes after startup check`() = runBlocking {
|
||||
val gate = SimklRefreshGate()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
package com.nuvio.app.features.watchprogress
|
||||
|
||||
import com.nuvio.app.features.tracking.TrackingProviderId
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TrackingProviderRefreshCoordinatorTest {
|
||||
@Test
|
||||
fun `successful active provider refresh republishes read models in order`() = runBlocking {
|
||||
val events = mutableListOf<String>()
|
||||
|
||||
val result = coordinateTrackingProviderRefresh(
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
refreshProvider = {
|
||||
events += "provider"
|
||||
true
|
||||
},
|
||||
activeProviderId = {
|
||||
events += "source"
|
||||
TrackingProviderId.SIMKL
|
||||
},
|
||||
refreshActiveReadModels = {
|
||||
events += "read-models"
|
||||
true
|
||||
},
|
||||
)
|
||||
|
||||
assertTrue(result)
|
||||
assertEquals(listOf("provider", "source", "read-models"), events)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `inactive provider refresh does not refresh another source`() = runBlocking {
|
||||
var readModelRefreshes = 0
|
||||
|
||||
val result = coordinateTrackingProviderRefresh(
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
refreshProvider = { true },
|
||||
activeProviderId = { TrackingProviderId.TRAKT },
|
||||
refreshActiveReadModels = {
|
||||
readModelRefreshes += 1
|
||||
true
|
||||
},
|
||||
)
|
||||
|
||||
assertTrue(result)
|
||||
assertEquals(0, readModelRefreshes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed provider refresh does not publish read models`() = runBlocking {
|
||||
var sourceReads = 0
|
||||
var readModelRefreshes = 0
|
||||
|
||||
val result = coordinateTrackingProviderRefresh(
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
refreshProvider = { false },
|
||||
activeProviderId = {
|
||||
sourceReads += 1
|
||||
TrackingProviderId.SIMKL
|
||||
},
|
||||
refreshActiveReadModels = {
|
||||
readModelRefreshes += 1
|
||||
true
|
||||
},
|
||||
)
|
||||
|
||||
assertFalse(result)
|
||||
assertEquals(0, sourceReads)
|
||||
assertEquals(0, readModelRefreshes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read model refresh failure is surfaced`() = runBlocking {
|
||||
val result = coordinateTrackingProviderRefresh(
|
||||
providerId = TrackingProviderId.SIMKL,
|
||||
refreshProvider = { true },
|
||||
activeProviderId = { TrackingProviderId.SIMKL },
|
||||
refreshActiveReadModels = { false },
|
||||
)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue