fix(#1286): handle corrupt session storage in session lists (#1298)

This commit is contained in:
m57 authored and GitHub committed 2026-09-26 22:42:01 +03:00
1 parent 45de69a271
commit 7ae97a1099
5 files changed
+118 -4

No files matched your search

@@ -63,6 +63,7 @@ data class SessionListResponse(
val total: Int = 0,
val limit: Int = 0,
val offset: Int = 0,
val storage: Map<String, String> = emptyMap(),
)
@Serializable
@@ -711,6 +711,17 @@ fun SessionsScreen(
)
}
state.sessions.isEmpty() && state.corruptStorageProfiles.isNotEmpty() -> {
ErrorState(
message =
stringResource(
R.string.sessions_storage_corrupt,
state.corruptStorageProfiles.sorted().joinToString(", "),
),
onRetry = { viewModel.loadSessions(forceRefresh = true) },
)
}
state.sessions.isEmpty() -> {
EmptyState(
title =
@@ -752,6 +763,18 @@ fun SessionsScreen(
else -> {
Column(modifier = Modifier.fillMaxSize()) {
if (state.corruptStorageProfiles.isNotEmpty()) {
Text(
text =
stringResource(
R.string.sessions_storage_corrupt,
state.corruptStorageProfiles.sorted().joinToString(", "),
),
color = statusColors.warning,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(spacing.md),
)
}
// ── Stats row ───────────────────────────────────────
SessionsStatsRow(
total = state.total,
@@ -81,6 +81,7 @@ data class SessionsUiState(
val total: Int = 0,
val hasMore: Boolean = false,
val errorMessage: String? = null,
val corruptStorageProfiles: Set<String> = emptySet(),
val stats: SessionStats = SessionStats(),
val isLoadingStats: Boolean = false,
val statsError: String? = null,
@@ -197,6 +198,7 @@ class SessionsViewModel(
val (requestScope, capturedSection) = scopeAndSection
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
if (requestGeneration == generation && currentScope == requestScope) {
if (handleStorageHealth(data)) return@refreshOnChange
val localKey = "${capturedSection.name}:${capturedSection.source}:${capturedSection.excludeSources}"
val inMemoryKey = requestScope?.inMemoryKey(localKey)
val persistentKey = requestScope?.persistentKey(localKey)
@@ -244,6 +246,7 @@ class SessionsViewModel(
selectedIds = emptySet(),
isSelecting = false,
errorMessage = null,
corruptStorageProfiles = emptySet(),
searchResults = emptyList(),
searchError = null,
searchLoadMoreError = null,
@@ -342,6 +345,7 @@ class SessionsViewModel(
total = 0,
hasMore = false,
errorMessage = null,
corruptStorageProfiles = emptySet(),
isSelecting = false,
selectedIds = emptySet(),
isSearching = false,
@@ -352,6 +356,26 @@ class SessionsViewModel(
if (query.isBlank()) loadSessions() else setSearchQuery(query)
}
// #1286: a partial/empty damaged-store page is not an authoritative replacement.
private fun handleStorageHealth(data: SessionListResponse): Boolean {
val corruptProfiles = data.storage.filterValues { it == "corrupt" }.keys
_uiState.update { state ->
if (corruptProfiles.isEmpty()) {
state.copy(corruptStorageProfiles = emptySet())
} else {
state.copy(
isLoading = false,
isLoadingMore = false,
errorMessage = null,
corruptStorageProfiles = corruptProfiles,
sessions = (state.sessions + data.sessions).distinctBy { it.id },
hasMore = false,
)
}
}
return corruptProfiles.isNotEmpty()
}
/** Load (or reload) sessions from page 0. Used by pull-to-refresh and initial load. */
fun loadSessions(forceRefresh: Boolean = false) {
val requestGeneration = generation
@@ -360,10 +384,7 @@ class SessionsViewModel(
val localKey = "${section.name}:${section.source}:${section.excludeSources}"
val inMemoryKey = requestScope?.inMemoryKey(localKey)
val persistentKey = requestScope?.persistentKey(localKey)
if (forceRefresh) {
if (inMemoryKey != null) sessionsPageCache.remove(inMemoryKey)
if (persistentKey != null) SessionListCacheStore.remove(persistentKey)
}
// #1286: keep the last good cache until a healthy refresh replaces it.
val cached =
if (!forceRefresh && inMemoryKey != null && persistentKey != null) {
sessionsPageCache.get(inMemoryKey)
@@ -418,6 +439,7 @@ class SessionsViewModel(
onSuccess = { data ->
val currentScope = runCatching { AuthManager.currentDataScope() }.getOrNull()
if (requestGeneration != generation || currentScope != requestScope) return@safeLaunchLoad
if (handleStorageHealth(data)) return@safeLaunchLoad
if (inMemoryKey != null) sessionsPageCache.put(inMemoryKey, data)
if (persistentKey != null) SessionListCacheStore.put(persistentKey, data)
rawPaginationOffset = data.nextOffset(0)
@@ -495,6 +517,7 @@ class SessionsViewModel(
when (result) {
is NetworkResult.Success -> {
val data = result.data
if (handleStorageHealth(data)) return@launch
rawPaginationOffset = data.nextOffset(offset)
_uiState.update {
val newSessions =