💾 Cache

KNET oferuje różne rozwiązania cache: cache w pamięci (KNETResponseCache), persistent cache na dysku (KNETPersistentCache) oraz warunkowy cache (KNETConditionalCache).

Cache Interceptor (w pamięci)

import rip.nerd.kitsunenet.interceptor.KNETCacheInterceptor

// Automatyczny cache dla requestów GET/HEAD via interceptor
val client = KNETClient.builder()
    .addInterceptor(KNETCacheInterceptor(
        maxAge = 300_000,   // TTL 5 minut
        maxSize = 100       // maks. 100 wpisów
    ))
    .build()

// Metody fabryczne
val client = KNETClient.builder()
    .addInterceptor(KNETCacheInterceptor.shortLived())   // 1 minuta
    .build()

KNETCacheInterceptor.mediumLived()  // 5 minut
KNETCacheInterceptor.longLived()    // 1 godzina

// Nagłówek X-KNET-Cache dodawany automatycznie:
// X-KNET-Cache: HIT  (z cache)
// X-KNET-Cache: MISS (z sieci)

Response Cache (ręcznie)

import rip.nerd.kitsunenet.cache.KNETResponseCache

val cache = KNETResponseCache(
    maxSize = 100,            // Maks. 100 wpisów
    defaultTtlMs = 300_000    // TTL 5 minut
)

// Zapisz odpowiedź
cache.put("https://api.example.com/users", response)

// Zapisz z własnym TTL
cache.put("https://api.example.com/users", response, ttlMs = 60_000)

// Pobierz
val cached = cache.get("https://api.example.com/users")

// Pobierz lub wykonaj request (async)
val response = cache.getOrPut("https://api.example.com/users") {
    client.get("https://api.example.com/users")
}

// Usuń wpis
cache.remove("https://api.example.com/users")

// Usuń po wzorcu
cache.removeByPattern(Regex(".*api.example.com/users.*"))

// Wyczyść wszystko
cache.clear()

// Sprawdź czy klucz istnieje (i nie wygasł)
val exists = cache.contains("https://api.example.com/users")

// Metody fabryczne
KNETResponseCache.shortLived()      // TTL 30s, 50 wpisów
KNETResponseCache.longLived()       // TTL 5 min, 200 wpisów
KNETResponseCache.staticResources() // TTL 1 godzina, 500 wpisów

Persistent Cache (dysk)

import rip.nerd.kitsunenet.cache.KNETPersistentCache

// Przetrwa restart aplikacji - zapisany w SharedPreferences
val cache = KNETPersistentCache.Builder(context)
    .maxMemoryEntries(100)
    .maxDiskEntries(500)
    .defaultTtlMs(3600_000)   // 1 godzina
    .encryptData(true)        // Opcjonalnie: szyfrowanie danych
    .build()

// Zapisz odpowiedź
cache.put("users_list", response)
cache.put("user_profile", response, ttlMs = 86400_000) // 24h

// Pobierz odpowiedź
val cached = cache.getResponse("users_list")

// Pobierz lub wykonaj request
val response = cache.getOrFetch("users_list") {
    client.get("https://api.example.com/users")
}

// Invalidacja
cache.invalidate("users_list")
cache.invalidateByPrefix("user_")
cache.invalidateAll()

// Stałe TTL
KNETPersistentCache.TTL.SHORT       // 1 minuta
KNETPersistentCache.TTL.MEDIUM      // 5 minut
KNETPersistentCache.TTL.LONG        // 1 godzina
KNETPersistentCache.TTL.VERY_LONG   // 24 godziny
KNETPersistentCache.TTL.WEEK        // 7 dni

Warunkowy Cache (ETag)

import rip.nerd.kitsunenet.cache.KNETConditionalCache

val conditionalCache = KNETConditionalCache()

// Automatycznie używa If-None-Match / If-Modified-Since
// Jeśli serwer zwróci 304 Not Modified, zwraca cached response
val response = conditionalCache.getWithValidation(client, request)

// ETag i Last-Modified są zapisywane i wysyłane automatycznie

Statystyki cache

val stats = cache.getStats()

println("Wpisy: ${stats.size} / ${stats.maxSize}")
println("Trafienia: ${stats.hits}")
println("Chybienia: ${stats.misses}")
println("Hit rate: ${String.format("%.1f", stats.hitRate * 100)}%")

println(stats.format())

Wstępne wypełnianie cache

// Załaduj cache przed interakcją użytkownika
suspend fun warmCache() {
    val urls = listOf(
        "https://api.example.com/config",
        "https://api.example.com/categories",
        "https://api.example.com/featured"
    )

    urls.forEach { url ->
        try {
            val response = client.get(url)
            cache.put(url, response)
        } catch (e: Exception) {
            Log.w("Cache", "Błąd wstępnego ładowania: $url")
        }
    }
}

Przykład: Offline-First

class OfflineFirstRepository(
    private val client: KNETClient,
    private val cache: KNETPersistentCache,
    private val networkChecker: NetworkChecker
) {

    suspend fun getUsers(): List<User> {
        // 1. Sprawdź cache
        val cached = cache.getResponse("users")

        // 2. Jeśli offline, zwróć cache
        if (!networkChecker.isOnline()) {
            return cached?.jsonList() ?: emptyList()
        }

        // 3. Pobierz z sieci
        return try {
            val response = client.get("$baseUrl/users")
            // 4. Zapisz do cache
            cache.put("users", response)
            response.jsonList()
        } catch (e: Exception) {
            // 5. Fallback do cache
            cached?.jsonList() ?: throw e
        }
    }
}

📚 Zobacz też