perf: optimize screen load times with progressive un-gating and SWR caching (#1192)

This commit is contained in:
M57 authored and GitHub committed 2026-09-18 23:25:30 +03:00
1 parent abce60ad7a
commit f5eaecbbef
55 files changed
+2494 -401

No files matched your search

@@ -71,6 +71,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.model.McpCatalogEntry
import com.m57.hermescontrol.data.model.McpServer
import com.m57.hermescontrol.theme.LocalSpacing
@@ -99,6 +100,7 @@ fun McpServersScreen(
viewModel: McpServersViewModel = viewModel { McpServersViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
val spacing = LocalSpacing.current
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
@@ -127,14 +129,17 @@ fun McpServersScreen(
}
}
LaunchedEffect(Unit) { viewModel.loadServers() }
LaunchedEffect(dataScope) {
viewModel.clearScopeOwnedState()
viewModel.loadServers()
}
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
HermesScaffold(
title = { Text(stringResource(R.string.screen_mcp_servers)) },
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadServers() },
onRefresh = { viewModel.loadServers(forceRefresh = true) },
actions = {
IconButton(
onClick = { viewModel.testAllServers() },
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.mcp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.local.SwrCache
import com.m57.hermescontrol.data.model.AddMcpServerRequest
import com.m57.hermescontrol.data.model.McpCatalogEntry
import com.m57.hermescontrol.data.model.McpCatalogInstallRequest
@@ -9,11 +10,13 @@ import com.m57.hermescontrol.data.model.McpOAuthFlowResponse
import com.m57.hermescontrol.data.model.McpServer
import com.m57.hermescontrol.data.model.McpServerTestResponse
import com.m57.hermescontrol.data.model.McpServerToggleRequest
import com.m57.hermescontrol.data.model.McpServersResponse
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.ui.common.ToastHost
import com.m57.hermescontrol.ui.common.safeLaunchLoad
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -69,10 +72,39 @@ class McpServersViewModel :
private val _uiState = MutableStateFlow(McpServersUiState())
val uiState: StateFlow<McpServersUiState> = _uiState.asStateFlow()
private val mcpCache = SwrCache<String, McpServersResponse>()
fun clearScopeOwnedState() {
_uiState.update {
it.copy(
isLoading = false,
servers = emptyList(),
serverTestResults = emptyMap(),
testingServers = emptySet(),
isTestingAll = false,
errorMessage = null,
activeOAuthFlow = null,
)
}
}
// ── Data loading ──────────────────────────────────────────
fun loadServers() {
safeLaunchLoad(
fun loadServers(forceRefresh: Boolean = false) {
safeLaunchSwrLoad(
cache = mcpCache,
forceRefresh = forceRefresh,
onCacheHit = { data ->
_uiState.update {
it.copy(
isLoading = false,
servers = data.servers.orEmpty(),
serverTestResults = emptyMap(),
testingServers = emptySet(),
errorMessage = null,
)
}
},
apiCall = { safeApiCall { ApiClient.hermesApi.getMcpServers() } },
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
@@ -247,7 +279,7 @@ class McpServersViewModel :
when (result) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Server '$name' deleted") }
loadServers()
loadServers(forceRefresh = true)
}
is NetworkResult.Failure -> {
@@ -305,7 +337,9 @@ class McpServersViewModel :
toastMessage = msg,
)
}
loadServers()
if (successCount > 0) {
loadServers(forceRefresh = true)
}
}
}
@@ -371,7 +405,7 @@ class McpServersViewModel :
toastMessage = "Server '${request.name}' added",
)
}
loadServers()
loadServers(forceRefresh = true)
}
is NetworkResult.Failure -> {
@@ -428,7 +462,7 @@ class McpServersViewModel :
toastMessage = "Env var '$key' added",
)
}
loadServers()
loadServers(forceRefresh = true)
}
is NetworkResult.Failure -> {
@@ -453,7 +487,7 @@ class McpServersViewModel :
when (result) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Env var '$key' removed") }
loadServers()
loadServers(forceRefresh = true)
}
is NetworkResult.Failure -> {
@@ -520,7 +554,7 @@ class McpServersViewModel :
toastMessage = "Catalog entry '${entry.name}' installed",
)
}
loadServers()
loadServers(forceRefresh = true)
}
is NetworkResult.Failure -> {
@@ -605,7 +639,7 @@ class McpServersViewModel :
toastMessage = "OAuth authorization successful!",
)
}
loadServers()
loadServers(forceRefresh = true)
}
"error" -> {