Add cache-first loading and background refresh for homepage directory

This commit is contained in:
Ris-git 2026-06-09 12:05:21 +05:30
parent af1d855ea1
commit 595f3848af
3 changed files with 121 additions and 11 deletions

View File

@ -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
}
}
/**

View File

@ -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<ServerData>? {
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<ServerData>) {
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)
}

View File

@ -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<ServerData>? = 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
}
}
}