Add cache-first loading and background refresh for homepage directory
This commit is contained in:
parent
af1d855ea1
commit
595f3848af
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue