From 595f3848af14f63d553195a14e568b3cd3ae64f4 Mon Sep 17 00:00:00 2001 From: Ris-git Date: Tue, 9 Jun 2026 12:05:21 +0530 Subject: [PATCH] Add cache-first loading and background refresh for homepage directory --- .../java/chat/zekochat/PeptideApplication.kt | 27 +++++++++ .../googlesheets/ServerDataRepository.kt | 50 ++++++++++++++++- .../discover/DiscoverServersListViewModel.kt | 55 +++++++++++++++---- 3 files changed, 121 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/chat/zekochat/PeptideApplication.kt b/app/src/main/java/chat/zekochat/PeptideApplication.kt index e1d75317..4d225e76 100644 --- a/app/src/main/java/chat/zekochat/PeptideApplication.kt +++ b/app/src/main/java/chat/zekochat/PeptideApplication.kt @@ -4,6 +4,12 @@ import android.app.Application import android.os.Looper import android.util.Log import chat.zekochat.api.PeptideHttp +import chat.zekochat.api.routes.googlesheets.ServerDataRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import chat.zekochat.api.realtime.DisconnectionState import chat.zekochat.api.realtime.RealtimeSocket import chat.zekochat.persistence.KVStorage @@ -25,6 +31,27 @@ class PeptideApplication : Application() { AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = LogPriority.VERBOSE) installNetworkUncaughtExceptionHandler() PeptideHttp // Trigger initialization + // Background prefetch for discover servers: only fetch if cache older than 10 minutes. + try { + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope.launch { + try { + val repo = ServerDataRepository() + val lastTs = try { repo.getLastFetchTs() } catch (_: Exception) { null } + val now = System.currentTimeMillis() + val STALE_MS = 10 * 60 * 1000L // 10 minutes + if (lastTs == null || (now - lastTs >= STALE_MS)) { + val csvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" + // Collect first emission to trigger repository fetch and cache save. + repo.getServers(csvUrl).first() + } + } catch (_: Exception) { + // best-effort prefetch; ignore failures + } + } + } catch (_: Exception) { + // ignore + } } /** diff --git a/app/src/main/java/chat/zekochat/api/routes/googlesheets/ServerDataRepository.kt b/app/src/main/java/chat/zekochat/api/routes/googlesheets/ServerDataRepository.kt index 4d46bb39..f904c3b6 100644 --- a/app/src/main/java/chat/zekochat/api/routes/googlesheets/ServerDataRepository.kt +++ b/app/src/main/java/chat/zekochat/api/routes/googlesheets/ServerDataRepository.kt @@ -1,7 +1,9 @@ package chat.zekochat.api.routes.googlesheets +import chat.zekochat.PeptideApplication import chat.zekochat.api.PeptideHttp import chat.zekochat.api.PeptideJson +import chat.zekochat.persistence.KVStorage import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import kotlinx.coroutines.Dispatchers @@ -9,6 +11,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer /** * Repository for fetching server data. Tries the Public Servers API first, @@ -16,6 +19,41 @@ import kotlinx.serialization.Serializable */ class ServerDataRepository { + private val CACHE_KEY = "discover/servers_cache" + private val CACHE_TS_KEY = "discover/servers_cache_ts" + + // Read cached servers from KVStorage (DataStore). Returns null on parse error or missing + suspend fun getCachedServers(): List? { + try { + val kv = KVStorage(PeptideApplication.instance) + val json = kv.get(CACHE_KEY) ?: return null + return PeptideJson.decodeFromString(ListSerializer(ServerData.serializer()), json) + } catch (_: Exception) { + return null + } + } + + // Save servers list and update timestamp + suspend fun saveServersToCache(servers: List) { + try { + val kv = KVStorage(PeptideApplication.instance) + val json = PeptideJson.encodeToString(ListSerializer(ServerData.serializer()), servers) + kv.set(CACHE_KEY, json) + kv.set(CACHE_TS_KEY, System.currentTimeMillis().toString()) + } catch (_: Exception) { + // best-effort cache; ignore failures + } + } + + suspend fun getLastFetchTs(): Long? { + try { + val kv = KVStorage(PeptideApplication.instance) + return kv.get(CACHE_TS_KEY)?.toLongOrNull() + } catch (_: Exception) { + return null + } + } + /** * Fetches server data from the public API first, falling back to Google Sheets. * @param sheetUrl The URL of the published Google Sheet (CSV or JSON) used as fallback @@ -42,6 +80,11 @@ class ServerDataRepository { ) } + // Update cache (best-effort) then emit + try { + saveServersToCache(mapped) + } catch (_: Exception) {} + emit(mapped) return@flow } @@ -68,7 +111,12 @@ class ServerDataRepository { ) } - emit(servers.sortedBy { it.sortOrder ?: Int.MAX_VALUE }) + val sorted = servers.sortedBy { it.sortOrder ?: Int.MAX_VALUE } + try { + saveServersToCache(sorted) + } catch (_: Exception) {} + + emit(sorted) }.flowOn(Dispatchers.IO) } diff --git a/app/src/main/java/chat/zekochat/composables/screens/chat/discover/DiscoverServersListViewModel.kt b/app/src/main/java/chat/zekochat/composables/screens/chat/discover/DiscoverServersListViewModel.kt index 36fe271e..6b6129bd 100644 --- a/app/src/main/java/chat/zekochat/composables/screens/chat/discover/DiscoverServersListViewModel.kt +++ b/app/src/main/java/chat/zekochat/composables/screens/chat/discover/DiscoverServersListViewModel.kt @@ -56,20 +56,55 @@ class DiscoverServersListViewModel @Inject constructor( */ fun loadServers() { updateState { it.copy(isLoading = true, error = null) } - + + val STALE_MS = 10 * 60 * 1000L // 10 minutes + val csvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" + viewModelScope.launch { + var cached: List? = null + try { - val csvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv" - serverDataRepository.getServers(csvUrl).collect { servers -> - updateState { it.copy(servers = servers, isLoading = false) } + cached = serverDataRepository.getCachedServers() + } catch (_: Exception) { + cached = null + } + + if (cached != null && cached.isNotEmpty()) { + updateState { it.copy(servers = cached, isLoading = false) } + } + + // Decide whether to refresh + val lastTs = try { serverDataRepository.getLastFetchTs() } catch (_: Exception) { null } + val now = System.currentTimeMillis() + val shouldRefresh = lastTs == null || (now - lastTs >= STALE_MS) + + if (cached == null) { + // No cache -> block until first network result (preserve existing behavior) + try { + serverDataRepository.getServers(csvUrl).collect { servers -> + updateState { it.copy(servers = servers, isLoading = false) } + } + } catch (e: Exception) { + updateState { + it.copy( + error = e.message ?: "Unknown error occurred", + isLoading = false + ) + } } - } catch (e: Exception) { - updateState { - it.copy( - error = e.message ?: "Unknown error occurred", - isLoading = false - ) + } else if (shouldRefresh) { + // Cache present but stale -> refresh in background and update when done + viewModelScope.launch { + try { + serverDataRepository.getServers(csvUrl).collect { servers -> + updateState { it.copy(servers = servers, isLoading = false) } + } + } catch (_: Exception) { + // keep showing cached data + } } + } else { + // Cache present and fresh -> nothing to do } } }