KNETVaryCache to zaawansowany cache HTTP z obsługą nagłówka
Vary. Automatycznie rozróżnia warianty odpowiedzi na podstawie nagłówków requestu.
Nagłówek Vary w odpowiedzi HTTP informuje, że odpowiedź może się różnić
w zależności od wartości określonych nagłówków requestu. Na przykład
Vary: Accept-Language oznacza, że treść może być inna dla różnych języków.
import rip.nerd.kitsunenet.cache.KNETVaryCache
// Utwórz cache
val cache = KNETVaryCache(
maxEntries = 100,
defaultTtlMs = 300_000 // 5 minut
)
// Wykonaj request z cache
val response = cache.get(client, request)
// Przy kolejnym wywołaniu z tym samym requestem
// zostanie zwrócona odpowiedź z cache (jeśli nie wygasła)
// Cache automatycznie rozróżnia warianty:
// GET /users Accept-Language: en -> cache key: "GET:/users|Accept-Language=en"
// GET /users Accept-Language: pl -> cache key: "GET:/users|Accept-Language=pl"
// Request 1: Pobierz stronę po angielsku
val request1 = KNETRequest(
url = "https://api.example.com/content",
headers = mapOf("Accept-Language" to "en")
)
val response1 = cache.get(client, request1)
// Response zawiera: Vary: Accept-Language
// Cache zapisuje: wariant dla Accept-Language=en
// Request 2: Pobierz stronę po polsku
val request2 = KNETRequest(
url = "https://api.example.com/content",
headers = mapOf("Accept-Language" to "pl")
)
val response2 = cache.get(client, request2)
// To jest NOWY wariant, więc wykonuje request
// Cache zapisuje: wariant dla Accept-Language=pl
// Request 3: Ponownie po angielsku
val request3 = KNETRequest(
url = "https://api.example.com/content",
headers = mapOf("Accept-Language" to "en")
)
val response3 = cache.get(client, request3)
// HIT! Zwraca z cache wariant Accept-Language=en
Typowe nagłówki używane z Vary:
| Nagłówek | Użycie |
|---|---|
Accept-Language |
Różne wersje językowe |
Accept-Encoding |
Kompresja (gzip, br) |
Accept |
Format odpowiedzi (JSON, XML) |
Authorization |
Różne dane per użytkownik |
User-Agent |
Różne wersje dla urządzeń |
get()Pobiera odpowiedź z cache lub wykonuje request.
suspend fun get(
client: KNETClient,
request: KNETRequest,
forceRefresh: Boolean = false
): KNETResponse
// Normalne użycie - sprawdza cache
val response = cache.get(client, request)
// Wymuś odświeżenie (pomija cache)
val freshResponse = cache.get(client, request, forceRefresh = true)
getFromCache()Pobiera wpis z cache bez wykonywania requestu.
fun getFromCache(request: KNETRequest): CacheEntry?
val cached = cache.getFromCache(request)
if (cached != null && !cached.isExpired) {
// Użyj cache
val response = cached.response
Log.d("Cache", "HIT - wiek: ${cached.age}ms")
} else {
// Cache miss lub wygasł
val response = client.request(request)
}
put()Ręcznie zapisuje odpowiedź do cache.
fun put(
request: KNETRequest,
response: KNETResponse,
ttlMs: Long = defaultTtlMs
)
// Zapisz z domyślnym TTL
cache.put(request, response)
// Zapisz z custom TTL (1 godzina)
cache.put(request, response, ttlMs = 3600_000)
// Krótki cache (1 minuta)
cache.put(request, response, ttlMs = 60_000)
invalidate()Usuwa wpis z cache.
fun invalidate(request: KNETRequest)
invalidateVariant()Usuwa konkretny wariant.
fun invalidateVariant(request: KNETRequest, varyHeaders: List<String>)
invalidateByPattern()Usuwa wpisy pasujące do wzorca URL.
fun invalidateByPattern(urlPattern: String)
// Usuń wszystkie wpisy dla /users/*
cache.invalidateByPattern(".*api.example.com/users.*")
// Usuń wszystkie wpisy dla konkretnego hosta
cache.invalidateByPattern(".*api.example.com.*")
clearExpired() / clear()fun clearExpired(): Int // Usuwa wygasłe, zwraca liczbę
fun clear() // Usuwa wszystko
contains()Sprawdza czy jest w cache.
fun contains(request: KNETRequest): Boolean
getStats()Pobiera statystyki cache.
fun getStats(): Stats
data class CacheEntry(
val response: KNETResponse,
val varyHeaders: List<String>, // Nagłówki z Vary
val variantKey: String, // Klucz wariantu
val createdAt: Long,
val expiresAt: Long,
val etag: String?, // ETag (opcjonalnie)
val lastModified: String? // Last-Modified (opcjonalnie)
) {
val isExpired: Boolean
val age: Long // Wiek w ms
}
data class Stats(
val totalEntries: Int, // Liczba kluczy cache
val totalVariants: Int, // Wszystkie warianty
val hits: Long, // Trafienia
val misses: Long, // Chybienia
val hitRate: Double, // % trafień
val expiredEntries: Int, // Wygasłe wpisy
val oldestEntryAge: Long, // Najstarszy wpis
val newestEntryAge: Long // Najnowszy wpis
)
val stats = cache.getStats()
println("=== Cache Stats ===")
println("Entries: ${stats.totalEntries}")
println("Variants: ${stats.totalVariants}")
println("Hits: ${stats.hits}")
println("Misses: ${stats.misses}")
println("Hit rate: ${"%.1f".format(stats.hitRate)}%")
println("Expired: ${stats.expiredEntries}")
// W UI
hitRateText.text = "Hit rate: ${"%.1f".format(stats.hitRate)}%"
progressBar.progress = stats.hitRate.toInt()
// Standardowy (100 entries, 5 min TTL)
val cache = KNETVaryCache.standard()
// Krótki TTL (1 minuta)
val cache = KNETVaryCache.shortLived()
// Długi TTL (1 godzina)
val cache = KNETVaryCache.longLived()
// Duży cache (500 entries)
val cache = KNETVaryCache.large()
| Preset | Max Entries | Default TTL | Użycie |
|---|---|---|---|
standard() |
100 | 5 min | Domyślne |
shortLived() |
100 | 1 min | Często zmieniające się dane |
longLived() |
100 | 1 godz | Statyczne dane |
large() |
500 | 5 min | Dużo różnych zasobów |
class LocalizedApiClient(
private val client: KNETClient,
private val locale: Locale
) {
private val cache = KNETVaryCache.standard()
suspend fun getContent(path: String): Content {
val request = KNETRequest(
url = "https://api.example.com$path",
headers = mapOf(
"Accept-Language" to locale.language
)
)
// Cache automatycznie rozróżni wersje językowe
val response = cache.get(client, request)
return response.json<Content>()
}
}
// Użycie
val plClient = LocalizedApiClient(client, Locale("pl"))
val enClient = LocalizedApiClient(client, Locale("en"))
// Te dwa wywołania będą cache'owane osobno
val plContent = plClient.getContent("/home")
val enContent = enClient.getContent("/home")
suspend fun getData(format: String): Any {
val request = KNETRequest(
url = "https://api.example.com/data",
headers = mapOf(
"Accept" to when (format) {
"json" -> "application/json"
"xml" -> "application/xml"
else -> "application/json"
}
)
)
return cache.get(client, request)
}
// Osobne warianty cache dla JSON i XML
val jsonData = getData("json")
val xmlData = getData("xml")
class SmartRepository(private val client: KNETClient) {
private val cache = KNETVaryCache(
maxEntries = 200,
defaultTtlMs = 600_000 // 10 minut
)
suspend fun getData(refresh: Boolean = false): Data {
val request = KNETRequest.get("https://api.example.com/data")
// Sprawdź cache przed requestem
if (!refresh) {
val cached = cache.getFromCache(request)
if (cached != null && !cached.isExpired) {
// Odśwież w tle jeśli wkrótce wygaśnie
if (cached.expiresAt - System.currentTimeMillis() < 60_000) {
refreshInBackground(request)
}
return cached.response.json()
}
}
// Wykonaj request
return cache.get(client, request, forceRefresh = refresh).json()
}
private fun refreshInBackground(request: KNETRequest) {
scope.launch {
try {
cache.get(client, request, forceRefresh = true)
} catch (e: Exception) {
// Ignoruj błędy odświeżania w tle
}
}
}
}
class UserDataCache(private val client: KNETClient) {
private val cache = KNETVaryCache.standard()
suspend fun getUserDashboard(userId: String, token: String): Dashboard {
val request = KNETRequest(
url = "https://api.example.com/dashboard",
headers = mapOf(
"Authorization" to "Bearer $token",
"X-User-Id" to userId
)
)
// Serwer zwraca: Vary: Authorization, X-User-Id
// Cache tworzy osobny wariant dla każdego użytkownika
return cache.get(client, request).json()
}
fun clearUserCache(userId: String) {
// Usuń cache tylko dla tego użytkownika
cache.invalidateByPattern(".*dashboard.*")
}
}
class CacheMonitor(private val cache: KNETVaryCache) {
fun logStats() {
val stats = cache.getStats()
Log.d("Cache", buildString {
appendLine("=== Vary Cache Stats ===")
appendLine("Entries: ${stats.totalEntries}")
appendLine("Variants: ${stats.totalVariants}")
appendLine("Hit rate: ${"%.2f".format(stats.hitRate)}%")
appendLine("Hits: ${stats.hits}, Misses: ${stats.misses}")
if (stats.hitRate < 50) {
appendLine("⚠️ Niski hit rate - rozważ dłuższy TTL")
}
if (stats.expiredEntries > stats.totalVariants / 2) {
appendLine("⚠️ Dużo wygasłych wpisów - wywołaj clearExpired()")
}
})
}
fun cleanupIfNeeded() {
val stats = cache.getStats()
if (stats.expiredEntries > 50) {
val removed = cache.clearExpired()
Log.d("Cache", "Usunięto $removed wygasłych wpisów")
}
}
}