ql Offline Queue - KNET

📴 Offline Queue

KNET automatycznie kolejkuje requesty gdy urządzenie jest offline i wysyła je po powrocie połączenia.

Szybki start

import rip.nerd.kitsunenet.sync.KNETBackgroundSync

val sync = KNETBackgroundSync(context)

// Dodaj request do kolejki offline
sync.enqueue(
    KNETRequest.post("https://api.example.com/orders", orderData)
)

// Request zostanie wysłany automatycznie
// gdy urządzenie będzie online

Konfiguracja

val sync = KNETBackgroundSync.Builder(context)
    .setRetryPolicy(RetryPolicy.EXPONENTIAL)
    .setMaxRetries(5)
    .setRequiredNetworkType(NetworkType.CONNECTED)  // lub UNMETERED
    .setRequireCharging(false)
    .setPersistQueue(true)  // Przetrwa restart
    .build()

// Start nasłuchiwania sieci
sync.start()

Enqueue z opcjami

sync.enqueue(
    request = KNETRequest.post(url, data),
    priority = Priority.HIGH,
    tag = "create-order",
    deadline = System.currentTimeMillis() + 3600_000,  // Max 1h
    onSuccess = { response ->
        showNotification("Zamówienie wysłane!")
    },
    onFailure = { error ->
        showNotification("Błąd: ${error.message}")
    }
)

Zarządzanie kolejką

// Sprawdź kolejkę
val pendingCount = sync.getPendingCount()
val pendingRequests = sync.getPendingRequests()

// Anuluj
sync.cancel(requestId)
sync.cancelByTag("create-order")
sync.cancelAll()

// Wymuś synchronizację
sync.syncNow()

// Status
val status = sync.getStatus()
// IDLE, SYNCING, WAITING_FOR_NETWORK, PAUSED

Nasłuchiwanie eventów

sync.addListener(object : SyncListener {
    override fun onSyncStarted() {
        showSyncIndicator()
    }

    override fun onSyncCompleted(successCount: Int, failedCount: Int) {
        hideSyncIndicator()
        showToast("Zsynchronizowano $successCount elementów")
    }

    override fun onRequestSent(request: KNETRequest, response: KNETResponse) {
        Log.d("Sync", "Sent: ${request.url}")
    }

    override fun onRequestFailed(request: KNETRequest, error: Throwable) {
        Log.e("Sync", "Failed: ${request.url} - ${error.message}")
    }

    override fun onNetworkAvailable() {
        Log.d("Sync", "Network available, starting sync...")
    }

    override fun onNetworkLost() {
        Log.d("Sync", "Network lost, queuing requests...")
    }
})

Conflict resolution

sync.setConflictResolver { localRequest, serverResponse ->
    // Obsłuż konflikt (np. 409 Conflict)
    when {
        serverResponse.statusCode == 409 -> {
            // Pobierz aktualną wersję z serwera
            val serverVersion = serverResponse.jsonObject()["version"] as Int
            val localVersion = (localRequest.data as Map<*, *>)["version"] as Int

            if (serverVersion > localVersion) {
                ConflictResolution.USE_SERVER
            } else {
                ConflictResolution.RETRY_WITH_MERGE
            }
        }
        else -> ConflictResolution.RETRY
    }
}

Offline Interceptor

// Alternatywnie - użyj interceptora
val interceptor = KNETOfflineInterceptor(
    isOnline = { networkChecker.isConnected() },
    offlineQueue = sync,
    queueableMethod = setOf("POST", "PUT", "DELETE"),
    fallbackToCache = true
)

val client = KNETClient.builder()
    .addInterceptor(interceptor)
    .build()

// Teraz POST/PUT/DELETE będą kolejkowane gdy offline

WorkManager integration

// Dla background sync użyj WorkManager
class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val sync = KNETBackgroundSync(applicationContext)

        return try {
            val result = sync.syncNow()
            if (result.failedCount == 0) {
                Result.success()
            } else {
                Result.retry()
            }
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

// Zaplanuj
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES
)
    .setConstraints(Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build())
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

Przykład: Offline-first app

class OrderRepository(
    private val client: KNETClient,
    private val sync: KNETBackgroundSync,
    private val localDb: OrderDatabase,
    private val networkChecker: NetworkChecker
) {

    suspend fun createOrder(order: Order): OrderResult {
        // 1. Zapisz lokalnie
        val localId = localDb.insertOrder(order.copy(
            status = OrderStatus.PENDING_SYNC
        ))

        // 2. Jeśli online - wyślij od razu
        if (networkChecker.isOnline()) {
            return try {
                val response = client.post("$baseUrl/orders", order.toJson())
                val serverOrder = response.json<Order>()

                // Update local z server ID
                localDb.updateOrder(localId, serverOrder)
                OrderResult.Success(serverOrder)

            } catch (e: Exception) {
                // Fallback do queue
                queueOrder(localId, order)
                OrderResult.Queued(localId)
            }
        } else {
            // 3. Offline - dodaj do kolejki
            queueOrder(localId, order)
            return OrderResult.Queued(localId)
        }
    }

    private fun queueOrder(localId: Long, order: Order) {
        sync.enqueue(
            request = KNETRequest.post("$baseUrl/orders", order.toJson()),
            tag = "order-$localId",
            onSuccess = { response ->
                val serverOrder = response.json<Order>()
                localDb.updateOrder(localId, serverOrder)
            }
        )
    }
}

sealed class OrderResult {
    data class Success(val order: Order) : OrderResult()
    data class Queued(val localId: Long) : OrderResult()
    data class Error(val message: String) : OrderResult()
}

📚 Zobacz też