📦 Batch Processor

KNETBatchProcessor pozwala na efektywne wykonywanie wielu requestów równolegle z pełną kontrolą nad concurrency, progress tracking i obsługą błędów.

💡 Kiedy używać?
  • Pobieranie wielu zasobów jednocześnie (np. lista użytkowników)
  • Synchronizacja danych z wieloma endpointami
  • Pre-loading danych w tle
  • Wykonywanie operacji na wielu elementach

📦 Import

import rip.nerd.kitsunenet.util.KNETBatchProcessor

🚀 Szybki start

// Utwórz procesor z max 5 równoczesnymi requestami
val processor = KNETBatchProcessor(maxConcurrent = 5)

// Przygotuj listę requestów
val requests = listOf(
    KNETRequest.get("https://api.example.com/users/1"),
    KNETRequest.get("https://api.example.com/users/2"),
    KNETRequest.get("https://api.example.com/users/3"),
    KNETRequest.get("https://api.example.com/users/4"),
    KNETRequest.get("https://api.example.com/users/5")
)

// Wykonaj wszystkie
val summary = processor.executeAll(client, requests)

// Sprawdź wyniki
println("Sukces: ${summary.successCount}/${summary.totalRequests}")
println("Błędy: ${summary.errorCount}")
println("Czas: ${summary.totalDurationMs}ms")

📊 BatchSummary

Wynik wykonania batch zawiera pełne statystyki:

data class BatchSummary(
    val results: List<BatchResult>,  // Wszystkie wyniki
    val totalRequests: Int,           // Liczba requestów
    val successCount: Int,            // Udane
    val errorCount: Int,              // Błędy
    val totalDurationMs: Long,        // Całkowity czas
    val averageDurationMs: Long       // Średni czas per request
) {
    val successRate: Double           // % sukcesu
    val successful: List<BatchResult> // Tylko udane
    val failed: List<BatchResult>     // Tylko błędne
}
data class BatchResult(
    val request: KNETRequest,    // Oryginalny request
    val response: KNETResponse?, // Odpowiedź (null przy błędzie)
    val error: Throwable?,       // Błąd (null przy sukcesie)
    val index: Int,              // Indeks w batch
    val durationMs: Long         // Czas wykonania
) {
    val isSuccess: Boolean       // Czy sukces
    val isError: Boolean         // Czy błąd
}

🔧 API Reference

executeAll()

Wykonuje wszystkie requesty równolegle (z limitem concurrency).

suspend fun executeAll(
    client: KNETClient,
    requests: List<KNETRequest>,
    onProgress: ProgressCallback? = null
): BatchSummary
Przykład z progress
val processor = KNETBatchProcessor(maxConcurrent = 3)

val summary = processor.executeAll(client, requests) { completed, total, result ->
    // Callback po każdym ukończonym requeście
    val percent = (completed * 100) / total

    runOnUiThread {
        progressBar.progress = percent
        statusText.text = "Pobieranie: $completed/$total"
    }

    if (result.isSuccess) {
        Log.d("Batch", "✅ ${result.request.url}")
    } else {
        Log.e("Batch", "❌ ${result.request.url}: ${result.error?.message}")
    }
}

// Podsumowanie
showToast("Pobrano ${summary.successCount} z ${summary.totalRequests} elementów")

executeAllFailFast()

Przerywa przy pierwszym błędzie.

suspend fun executeAllFailFast(
    client: KNETClient,
    requests: List<KNETRequest>
): BatchSummary
Przykład - krytyczne operacje
// Użyj fail-fast gdy wszystkie requesty muszą się powieść
val summary = processor.executeAllFailFast(client, requests)

if (summary.errorCount > 0) {
    // Przerwano na pierwszym błędzie
    val failedResult = summary.failed.first()
    showError("Operacja przerwana: ${failedResult.error?.message}")

    // Możesz zobaczyć ile udało się wykonać przed błędem
    Log.d("Batch", "Wykonano ${summary.successCount} przed błędem")
} else {
    // Wszystkie OK
    processResults(summary.successful)
}

executeAllSuccessful()

Zwraca tylko udane odpowiedzi.

suspend fun executeAllSuccessful(
    client: KNETClient,
    requests: List<KNETRequest>
): List<KNETResponse>
Przykład
// Pobierz tylko udane odpowiedzi
val responses = processor.executeAllSuccessful(client, requests)

// Mapuj na obiekty
val users = responses.map { it.json<User>() }

Log.d("Batch", "Pobrano ${users.size} użytkowników")

executeAndMap()

Wykonuje i mapuje wyniki na dowolny typ.

suspend fun <T> executeAndMap(
    client: KNETClient,
    requests: List<KNETRequest>,
    mapper: (KNETResponse) -> T
): List<T?>
Przykład
// Pobierz i od razu zmapuj na obiekty User
val users: List<User?> = processor.executeAndMap(client, requests) { response ->
    response.json<User>()
}

// Filtruj null-e (błędne requesty)
val validUsers = users.filterNotNull()
Log.d("Batch", "Pobrano ${validUsers.size} użytkowników")

executeForEach()

Generuje requesty z listy elementów.

suspend fun <T> executeForEach(
    client: KNETClient,
    items: List<T>,
    requestBuilder: (T) -> KNETRequest
): BatchSummary
Przykład
val userIds = listOf("user1", "user2", "user3", "user4", "user5")

val summary = processor.executeForEach(client, userIds) { userId ->
    KNETRequest.get("https://api.example.com/users/$userId")
}

// Lub z bardziej złożoną logiką
val itemsToUpdate = listOf(
    UpdateItem(id = 1, name = "New Name 1"),
    UpdateItem(id = 2, name = "New Name 2")
)

val updateSummary = processor.executeForEach(client, itemsToUpdate) { item ->
    KNETRequest(
        url = "https://api.example.com/items/${item.id}",
        method = "PUT",
        data = mapOf("name" to item.name)
    )
}

executeAllWithRetry()

Wykonuje z retry per request.

suspend fun executeAllWithRetry(
    client: KNETClient,
    requests: List<KNETRequest>,
    retries: Int = 3,
    onProgress: ProgressCallback? = null
): BatchSummary
Przykład
// Każdy request będzie ponawiany do 3 razy przy błędzie
val summary = processor.executeAllWithRetry(
    client,
    requests,
    retries = 3
) { completed, total, result ->
    Log.d("Batch", "Progress: $completed/$total")
}

// Nawet jeśli niektóre requesty początkowo zawiodą,
// retry może je uratować
Log.d("Batch", "Sukces: ${summary.successCount}/${summary.totalRequests}")

executeSequential()

Wykonuje sekwencyjnie (jeden po drugim).

suspend fun executeSequential(
    client: KNETClient,
    requests: List<KNETRequest>,
    onProgress: ProgressCallback? = null
): BatchSummary
Przykład - zachowanie kolejności
// Użyj gdy kolejność jest ważna lub API wymaga sekwencyjności
val summary = processor.executeSequential(client, requests)

// Wyniki są w tej samej kolejności co requesty
summary.results.forEachIndexed { index, result ->
    println("Request $index: ${if (result.isSuccess) "OK" else "FAIL"}")
}

executeGroupedByHost()

Grupuje requesty według hosta i wykonuje.

suspend fun executeGroupedByHost(
    client: KNETClient,
    requests: List<KNETRequest>
): Map<String, BatchSummary>
Przykład
val mixedRequests = listOf(
    KNETRequest.get("https://api.github.com/users/1"),
    KNETRequest.get("https://api.github.com/users/2"),
    KNETRequest.get("https://api.twitter.com/users/1"),
    KNETRequest.get("https://api.twitter.com/users/2")
)

val resultsByHost = processor.executeGroupedByHost(client, mixedRequests)

resultsByHost.forEach { (host, summary) ->
    println("$host: ${summary.successCount}/${summary.totalRequests}")
}

// Output:
// api.github.com: 2/2
// api.twitter.com: 2/2

🎯 Presety

// Standardowy (5 równoległych)
val processor = KNETBatchProcessor.standard()

// Dla urządzeń mobilnych (3 równoległe)
val processor = KNETBatchProcessor.mobile()

// Wysokiej przepustowości (10 równoległych)
val processor = KNETBatchProcessor.highThroughput()

// Throttled - max X requestów na sekundę
val processor = KNETBatchProcessor.throttled(
    maxConcurrent = 5,
    requestsPerSecond = 10
)

// Sekwencyjny (1 na raz)
val processor = KNETBatchProcessor.sequential()

// Z domyślnym retry
val processor = KNETBatchProcessor.withRetry(retries = 3)
Preset Max Concurrent Delay Retry Użycie
standard() 5 0 0 Domyślny
mobile() 3 0 0 Oszczędność baterii
highThroughput() 10 0 0 WiFi, szybkie API
throttled(5, 10) 5 100ms 0 API z rate limit
sequential() 1 0 0 Zachowanie kolejności
withRetry(3) 5 0 3 Niestabilne API

💡 Praktyczne przykłady

Pobieranie listy użytkowników

class UserRepository(private val client: KNETClient) {

    private val processor = KNETBatchProcessor.mobile()

    suspend fun fetchUsers(ids: List<String>): List<User> {
        val requests = ids.map { id ->
            KNETRequest.get("https://api.example.com/users/$id")
        }

        return processor.executeAndMap(client, requests) { response ->
            response.json<User>()
        }.filterNotNull()
    }

    suspend fun fetchUsersWithProgress(
        ids: List<String>,
        onProgress: (Int, Int) -> Unit
    ): List<User> {
        val requests = ids.map { id ->
            KNETRequest.get("https://api.example.com/users/$id")
        }

        val summary = processor.executeAll(client, requests) { completed, total, _ ->
            onProgress(completed, total)
        }

        return summary.successful.map { it.response!!.json<User>() }
    }
}

Synchronizacja wielu endpointów

class SyncService(private val client: KNETClient) {

    private val processor = KNETBatchProcessor(maxConcurrent = 3)

    suspend fun syncAll(): SyncResult {
        val endpoints = listOf(
            "https://api.example.com/users",
            "https://api.example.com/products",
            "https://api.example.com/orders",
            "https://api.example.com/settings"
        )

        val requests = endpoints.map { KNETRequest.get(it) }

        val summary = processor.executeAll(client, requests)

        return SyncResult(
            usersSync = summary.results[0].isSuccess,
            productsSync = summary.results[1].isSuccess,
            ordersSync = summary.results[2].isSuccess,
            settingsSync = summary.results[3].isSuccess,
            totalTime = summary.totalDurationMs
        )
    }
}

Batch update z UI

class BatchUpdateViewModel : ViewModel() {

    private val processor = KNETBatchProcessor.withRetry(2)

    private val _progress = MutableStateFlow(0f)
    val progress = _progress.asStateFlow()

    private val _status = MutableStateFlow<BatchStatus>(BatchStatus.Idle)
    val status = _status.asStateFlow()

    fun updateItems(items: List<Item>) {
        viewModelScope.launch {
            _status.value = BatchStatus.InProgress

            val requests = items.map { item ->
                KNETRequest(
                    url = "https://api.example.com/items/${item.id}",
                    method = "PUT",
                    data = item.toMap()
                )
            }

            val summary = processor.executeAll(client, requests) { completed, total, _ ->
                _progress.value = completed.toFloat() / total
            }

            _status.value = if (summary.errorCount == 0) {
                BatchStatus.Success(summary.successCount)
            } else {
                BatchStatus.PartialSuccess(
                    success = summary.successCount,
                    failed = summary.errorCount,
                    errors = summary.failed.map { it.error?.message ?: "Unknown" }
                )
            }
        }
    }
}

sealed class BatchStatus {
    object Idle : BatchStatus()
    object InProgress : BatchStatus()
    data class Success(val count: Int) : BatchStatus()
    data class PartialSuccess(
        val success: Int,
        val failed: Int,
        val errors: List<String>
    ) : BatchStatus()
}

⚠️ Obsługa błędów

val summary = processor.executeAll(client, requests)

// Sprawdź czy są błędy
if (summary.errorCount > 0) {
    // Pokaż błędy
    summary.failed.forEach { result ->
        Log.e("Batch", "Błąd dla ${result.request.url}: ${result.error?.message}")

        // Możesz spróbować ponownie tylko błędne
        val failedRequests = summary.failed.map { it.request }
        val retrySummary = processor.executeAllWithRetry(client, failedRequests, retries = 3)
    }
}

// Lub użyj when dla różnych scenariuszy
when {
    summary.errorCount == 0 -> {
        showSuccess("Wszystkie operacje zakończone!")
    }
    summary.successCount == 0 -> {
        showError("Wszystkie operacje nie powiodły się")
    }
    else -> {
        showWarning("${summary.successCount} OK, ${summary.errorCount} błędów")
    }
}

📚 Zobacz też