🔌 WebSocket

KNET oferuje pełną obsługę WebSocket z auto-reconnect, heartbeat i typowanymi wiadomościami.

Szybki start

val ws = KNETWebSocket.connect("wss://api.example.com/ws")

// Nasłuchuj wiadomości
ws.onMessage { message ->
    println("Otrzymano: $message")
}

// Nasłuchuj eventów
ws.onOpen {
    println("Połączono!")
    ws.send("Hello Server!")
}

ws.onClose { code, reason ->
    println("Zamknięto: $code - $reason")
}

ws.onError { error ->
    println("Błąd: ${error.message}")
}

// Wyślij wiadomość
ws.send("Hello!")

// Wyślij JSON
ws.sendJson(mapOf("type" to "ping"))

// Zamknij połączenie
ws.close()

Builder

val ws = KNETWebSocket.builder("wss://api.example.com/ws")
    .headers(mapOf(
        "Authorization" to "Bearer $token",
        "X-Client-ID" to clientId
    ))
    .connectTimeout(10_000)
    .readTimeout(0)  // Brak timeout dla WebSocket
    .pingInterval(30_000)  // Ping co 30s
    .autoReconnect(true)
    .reconnectDelay(5_000)
    .maxReconnectAttempts(10)
    .build()

// Połącz
ws.connect()

// Lub z callback
ws.connect { success, error ->
    if (success) {
        println("Połączono!")
    } else {
        println("Błąd: ${error?.message}")
    }
}

Auto-Reconnect

val ws = KNETWebSocket.builder(url)
    .autoReconnect(true)
    .reconnectDelay(5_000)        // Początkowy delay
    .maxReconnectDelay(60_000)    // Max delay (exponential backoff)
    .maxReconnectAttempts(10)     // Max prób (0 = unlimited)
    .build()

// Eventy reconnect
ws.onReconnecting { attempt, delay ->
    showNotification("Ponowne łączenie... próba $attempt")
}

ws.onReconnected {
    showNotification("Połączono ponownie!")
    resubscribeToChannels()
}

Typowane wiadomości

// Definiuj typy
data class ChatMessage(
    val type: String,
    val userId: String,
    val content: String,
    val timestamp: Long
)

// Nasłuchuj z deserializacją
ws.onMessage<ChatMessage> { message ->
    when (message.type) {
        "chat" -> displayMessage(message)
        "typing" -> showTypingIndicator(message.userId)
        "read" -> markAsRead(message)
    }
}

// Wyślij typowane
ws.sendJson(ChatMessage(
    type = "chat",
    userId = currentUserId,
    content = "Hello!",
    timestamp = System.currentTimeMillis()
))

Subskrypcje (Channels)

// Wzorzec pub/sub
ws.subscribe("chat:room123") { message ->
    displayChatMessage(message)
}

ws.subscribe("notifications") { notification ->
    showNotification(notification)
}

// Anuluj subskrypcję
ws.unsubscribe("chat:room123")

// Przykładowa implementacja
class WebSocketChannel(
    private val ws: KNETWebSocket,
    private val channel: String
) {
    fun subscribe() {
        ws.sendJson(mapOf(
            "action" to "subscribe",
            "channel" to channel
        ))
    }

    fun unsubscribe() {
        ws.sendJson(mapOf(
            "action" to "unsubscribe",
            "channel" to channel
        ))
    }

    fun send(data: Any) {
        ws.sendJson(mapOf(
            "action" to "message",
            "channel" to channel,
            "data" to data
        ))
    }
}

Flow API

// WebSocket jako Flow
val messagesFlow: Flow<String> = ws.asFlow()

// Użycie w ViewModel
class ChatViewModel : ViewModel() {

    private val ws = KNETWebSocket.connect(wsUrl)

    val messages: StateFlow<List<Message>> = ws.asFlow()
        .map { json -> parseMessage(json) }
        .scan(emptyList<Message>()) { acc, msg -> acc + msg }
        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

    fun sendMessage(text: String) {
        ws.sendJson(mapOf("text" to text))
    }

    override fun onCleared() {
        ws.close()
    }
}

Heartbeat / Ping-Pong

val ws = KNETWebSocket.builder(url)
    .pingInterval(30_000)  // Wyślij ping co 30s
    .pongTimeout(10_000)   // Czekaj max 10s na pong
    .build()

// Własna logika heartbeat
ws.onOpen {
    startHeartbeat()
}

private fun startHeartbeat() {
    heartbeatJob = scope.launch {
        while (isActive) {
            delay(30_000)
            ws.sendJson(mapOf("type" to "ping"))
        }
    }
}

ws.onMessage { message ->
    if (message.contains("pong")) {
        lastPongTime = System.currentTimeMillis()
    }
}

Binary data

// Wyślij binary
val imageBytes = loadImage()
ws.sendBinary(imageBytes)

// Odbierz binary
ws.onBinaryMessage { bytes ->
    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
    displayImage(bitmap)
}

Przykład: Chat

class ChatRepository(private val token: String) {

    private var ws: KNETWebSocket? = null
    private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
    val messages: StateFlow<List<ChatMessage>> = _messages

    private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
    val connectionState: StateFlow<ConnectionState> = _connectionState

    fun connect(roomId: String) {
        ws = KNETWebSocket.builder("wss://chat.example.com/rooms/$roomId")
            .headers(mapOf("Authorization" to "Bearer $token"))
            .autoReconnect(true)
            .build()

        ws?.onOpen {
            _connectionState.value = ConnectionState.Connected
        }

        ws?.onClose { _, _ ->
            _connectionState.value = ConnectionState.Disconnected
        }

        ws?.onReconnecting { attempt, _ ->
            _connectionState.value = ConnectionState.Reconnecting(attempt)
        }

        ws?.onMessage<ChatMessage> { message ->
            _messages.update { it + message }
        }

        ws?.connect()
    }

    fun sendMessage(text: String) {
        ws?.sendJson(ChatMessage(
            type = "message",
            content = text,
            userId = currentUserId,
            timestamp = System.currentTimeMillis()
        ))
    }

    fun disconnect() {
        ws?.close()
        ws = null
    }
}

sealed class ConnectionState {
    object Disconnected : ConnectionState()
    object Connected : ConnectionState()
    data class Reconnecting(val attempt: Int) : ConnectionState()
}

📚 Zobacz też