mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
fix: Prevent duplicate API calls on app startup
ensureReady() had a race condition where concurrent callers from init block and ViewModels would all run the loading logic simultaneously. Added Mutex to ensure only the first caller loads; others await. Also added a cooldown to refreshHomeData() since LifecycleResumeEffect fires immediately on first composition, triggering a redundant reload right after the initial load completes.
This commit is contained in:
@@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
@@ -45,7 +47,9 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
) : MediaRepository {
|
) : MediaRepository {
|
||||||
|
|
||||||
private val ready = CompletableDeferred<Unit>()
|
private val ready = CompletableDeferred<Unit>()
|
||||||
|
private val readyMutex = Mutex()
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private var initialLoadTimestamp = 0L
|
||||||
|
|
||||||
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Loading)
|
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Loading)
|
||||||
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
|
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
|
||||||
@@ -85,19 +89,34 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ensureReady() {
|
override suspend fun ensureReady() {
|
||||||
if (ready.isCompleted) return
|
if (ready.isCompleted) {
|
||||||
|
ready.await() // rethrows if completed exceptionally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Only the first caller runs the loading logic; others wait on the deferred.
|
||||||
loadLibraries()
|
if (readyMutex.tryLock()) {
|
||||||
loadContinueWatching()
|
try {
|
||||||
loadNextUp()
|
if (ready.isCompleted) {
|
||||||
loadLatestLibraryContent()
|
ready.await()
|
||||||
_state.value = MediaRepositoryState.Ready
|
return
|
||||||
ready.complete(Unit)
|
}
|
||||||
} catch (t: Throwable) {
|
loadLibraries()
|
||||||
_state.value = MediaRepositoryState.Error(t)
|
loadContinueWatching()
|
||||||
ready.completeExceptionally(t)
|
loadNextUp()
|
||||||
throw t
|
loadLatestLibraryContent()
|
||||||
|
_state.value = MediaRepositoryState.Ready
|
||||||
|
initialLoadTimestamp = System.currentTimeMillis()
|
||||||
|
ready.complete(Unit)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
_state.value = MediaRepositoryState.Error(t)
|
||||||
|
ready.completeExceptionally(t)
|
||||||
|
throw t
|
||||||
|
} finally {
|
||||||
|
readyMutex.unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ready.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +288,21 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
localDataSource.updateWatchProgress(mediaId, progressPercent, watched)
|
localDataSource.updateWatchProgress(mediaId, progressPercent, watched)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REFRESH_MIN_INTERVAL_MS = 30_000L
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun refreshHomeData() {
|
override suspend fun refreshHomeData() {
|
||||||
|
awaitReady()
|
||||||
|
// Skip refresh if the initial load (or last refresh) just happened
|
||||||
|
val elapsed = System.currentTimeMillis() - initialLoadTimestamp
|
||||||
|
if (elapsed < REFRESH_MIN_INTERVAL_MS) return
|
||||||
|
|
||||||
loadLibraries()
|
loadLibraries()
|
||||||
loadContinueWatching()
|
loadContinueWatching()
|
||||||
loadNextUp()
|
loadNextUp()
|
||||||
loadLatestLibraryContent()
|
loadLatestLibraryContent()
|
||||||
|
initialLoadTimestamp = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun serverUrl(): String {
|
private suspend fun serverUrl(): String {
|
||||||
|
|||||||
Reference in New Issue
Block a user