refactor(core/series): improve data loading reliability with concurrent initialization

This commit is contained in:
2026-06-20 12:18:29 +00:00
parent 5a98f3d2e7
commit 8a2702a7cb
4 changed files with 48 additions and 28 deletions

View File

@@ -139,26 +139,46 @@ class InMemoryLocalMediaRepository @Inject constructor(
private suspend fun loadSeasonsInternal(seriesId: UUID) {
seriesState.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
val seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
val updatedSeries = series.copy(
seasons = jellyfinApiClient.getSeasons(seriesId).map { it.toSeason() }
)
seriesState.update { it + (updatedSeries.id to updatedSeries) }
// Await the parallel loadSeries to populate the series entry.
// Precondition: seriesId refers to a real series (selectSeries always uses a
// SeriesDto from a real series list). If loadSeries returns without populating
// state (null item / wrong type) this suspends until cancelled by structured
// concurrency when a sibling load* call fails.
seriesState.first { it.containsKey(seriesId) }
seriesState.update { current ->
val existing = current[seriesId] ?: return@update current
if (existing.seasons.isNotEmpty()) return@update current
current + (seriesId to existing.copy(seasons = seasons))
}
}
override suspend fun loadSeasonEpisodes(seriesId: UUID, seasonId: UUID) {
loadSeasons(seriesId)
// Fast path: season already cached with episodes or known empty — skip the fetch.
seriesState.value[seriesId]?.seasons?.firstOrNull { it.id == seasonId }?.let { cached ->
if (cached.episodes.isNotEmpty() || cached.episodeCount == 0) return
}
val series = seriesState.value[seriesId] ?: throw RuntimeException("Series not found")
// Fetch first so this runs concurrently with loadSeries/loadSeasons when launched
// from selectSeries. Precondition: seasons are pre-loaded by selectSeries and
// seasonId is always a valid season from a real EpisodeDto in this path.
val serverUrl = userSessionRepository.serverUrl.first()
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
.map { it.toEpisode(serverUrl) }
// Await the season to be present in state (immediate when pre-loaded; waits for
// the parallel loadSeasons otherwise).
seriesState.first { it[seriesId]?.seasons?.any { it.id == seasonId } == true }
// Re-check guard after await: a concurrent call may have filled episodes already.
val series = seriesState.value[seriesId] ?: return
val season = series.seasons.firstOrNull { it.id == seasonId } ?: return
if (season.episodes.isNotEmpty() || season.episodeCount == 0) {
return
}
val serverUrl = userSessionRepository.serverUrl.first()
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
.map { it.toEpisode(serverUrl) }
seriesState.update { current ->
val currentSeries = current[seriesId] ?: return@update current
val updatedSeries = currentSeries.copy(