💾 Cache

KNET oferuje wielopoziomowy system cache: Memory, Disk, Persistent i Conditional (ETag).

Memory Cache

import rip.nerd.kitsunenet.cache.KNETMemoryCache

val cache = KNETMemoryCache(
    maxSize = 50,           // Max 50 wpisów
    defaultTtlMs = 300_000  // 5 minut TTL
)

// Użyj z interceptorem
val client = KNETClient.builder()
    .addInterceptor(KNETCacheInterceptor(cache))
    .build()

// Lub manualnie
cache.put(request, response)
val cached = cache.get(request)
cache.invalidate(request)
cache.clear()

Disk Cache

import rip.nerd.kitsunenet.cache.KNETDiskCache

val cache = KNETDiskCache(
    directory = context.cacheDir,
    maxSizeBytes = 50 * 1024 * 1024,  // 50 MB
    defaultTtlMs = 3600_000           // 1 godzina
)

// Przetrwa restart aplikacji
val client = KNETClient.builder()
    .addInterceptor(KNETCacheInterceptor(cache))
    .build()

Persistent Cache

import rip.nerd.kitsunenet.cache.KNETPersistentCache

// Cache, który nigdy nie wygasa (do offline)
val cache = KNETPersistentCache(context)

// Zapisz
cache.store("users", usersJson)

// Pobierz
val users = cache.retrieve("users")

// Z timestamp
val (data, timestamp) = cache.retrieveWithTimestamp("users")
if (System.currentTimeMillis() - timestamp > 3600_000) {
    // Dane starsze niż 1h - odśwież
}

Conditional Cache (ETag)

import rip.nerd.kitsunenet.cache.KNETConditionalCache

val cache = KNETConditionalCache()

// Automatycznie używa If-None-Match / If-Modified-Since
val response = cache.getWithValidation(client, request)

// Jeśli serwer zwróci 304 Not Modified,
// zwraca cached response bez pobierania body

// Zapisuje ETag i Last-Modified automatycznie

Cache Interceptor

val interceptor = KNETCacheInterceptor(
    cache = cache,
    defaultTtlMs = 300_000,
    cacheOnlyGet = true,          // Cache tylko GET
    respectCacheHeaders = true,   // Respektuj Cache-Control
    staleWhileRevalidate = true   // Zwróć stale, odśwież w tle
)

// Cache-Control headers obsługiwane:
// - max-age=300
// - no-cache
// - no-store
// - must-revalidate
// - private / public

Strategia cache

enum class CacheStrategy {
    CACHE_FIRST,      // Cache → Network (fallback)
    NETWORK_FIRST,    // Network → Cache (fallback)
    CACHE_ONLY,       // Tylko cache
    NETWORK_ONLY,     // Tylko network
    STALE_WHILE_REVALIDATE  // Cache + odśwież w tle
}

val client = KNETClient.builder()
    .cacheStrategy(CacheStrategy.STALE_WHILE_REVALIDATE)
    .build()

// Lub per request
val request = KNETRequest.get(url)
    .withCacheStrategy(CacheStrategy.NETWORK_FIRST)

Cache key customization

val cache = KNETMemoryCache(
    keyGenerator = { request ->
        // Domyślnie: method + url
        // Custom: uwzględnij headers
        buildString {
            append(request.method)
            append(":")
            append(request.url)
            request.headers["Accept-Language"]?.let {
                append(":lang=$it")
            }
        }
    }
)

Invalidacja

// Pojedynczy wpis
cache.invalidate(request)

// Po URL pattern
cache.invalidateByPattern("https://api.example.com/users.*")

// Po tagu
cache.invalidateByTag("users")

// Wszystko
cache.clear()

// Wygasłe wpisy
val removed = cache.clearExpired()
println("Usunięto $removed wygasłych wpisów")

Cache warming

// Pre-load cache
suspend fun warmCache() {
    val urls = listOf(
        "/api/config",
        "/api/categories",
        "/api/featured"
    )

    urls.forEach { path ->
        try {
            val response = client.get("$baseUrl$path")
            cache.put(KNETRequest.get("$baseUrl$path"), response)
        } catch (e: Exception) {
            Log.w("Cache", "Failed to warm: $path")
        }
    }
}

Statystyki

val stats = cache.getStats()

println("Entries: ${stats.size}")
println("Hits: ${stats.hits}")
println("Misses: ${stats.misses}")
println("Hit rate: ${stats.hitRate}%")
println("Memory used: ${stats.memorySizeBytes / 1024} KB")

// Monitorowanie
cache.onHit { request ->
    analytics.log("cache_hit", request.url)
}

cache.onMiss { request ->
    analytics.log("cache_miss", request.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.retrieve("users")

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

        // 3. Pobierz z sieci
        return try {
            val response = client.get("$baseUrl/users")
            val users = response.jsonList<User>()

            // 4. Zapisz do cache
            cache.store("users", response.bodyString)

            users
        } catch (e: Exception) {
            // 5. Fallback do cache
            cached?.let { parseUsers(it) } ?: throw e
        }
    }
}

📚 Zobacz też