🗃️ Vary Cache

KNETVaryCache to zaawansowany cache HTTP z obsługą nagłówka Vary. Automatycznie rozróżnia warianty odpowiedzi na podstawie nagłówków requestu.

💡 Co to jest nagłówek Vary?

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

import rip.nerd.kitsunenet.cache.KNETVaryCache

🚀 Szybki start

// 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"

📋 Jak działa Vary Cache?

Scenariusz
// 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ń

🔧 API Reference

get()

Pobiera odpowiedź z cache lub wykonuje request.

suspend fun get(
    client: KNETClient,
    request: KNETRequest,
    forceRefresh: Boolean = false
): KNETResponse
Przykład
// 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?
Przykład
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
)
Przykład
// 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)
Przykład
// 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

📊 CacheEntry i 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
)
Przykład - wyświetl statystyki
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()

🎯 Presety

// 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

💡 Praktyczne przykłady

Cache z lokalizacją

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")

Cache z różnymi formatami

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")

Inteligentne odświeżanie

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
            }
        }
    }
}

Cache per user

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.*")
    }
}

Monitorowanie cache

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")
        }
    }
}

⚠️ Kiedy NIE używać Vary Cache

⚠️ Uważaj przy
  • Vary: * - oznacza "nigdy nie cache'uj"
  • POST/PUT/DELETE - modyfikujące requesty nie powinny być cache'owane
  • Dane wrażliwe - cache trzymany w pamięci może być odczytany
  • Duże odpowiedzi - mogą zużyć dużo pamięci

📚 Zobacz też