mobiler 0.13.0

Build mobile apps in Rust — one core, native UI on Android, iOS, and the web (CLI)
package {{PACKAGE}}

import {{PACKAGE_SHARED_TYPES}}.PluginResponse

import kotlinx.coroutines.channels.Channel
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener

// WebSocket (free, bundled). A persistent connection bridged into the request/response ABI via
// four ops; the app pumps `recv` in a loop to stream incoming frames:
//   connect: input = the ws:// or wss:// URL           → ok:true once open (ok:false on failure)
//   send:    input = the text frame to send            → ok:true
//   recv:    input = ""  (suspends for the next frame) → ok:true, output = frame text;
//                                                          ok:false, output = "closed" when the
//                                                          socket closes (stop looping)
//   close:   input = ""                                → ok:true
// Backed by OkHttp's WebSocket (already a shell dependency). Incoming frames + close are queued in
// a Channel so `recv` never drops a message between calls. The plugin instance is a registry
// singleton, so it holds one connection for the app's lifetime.
class WebSocketPlugin(private val application: android.app.Application) : MobilerPlugin {
    private val client = OkHttpClient()
    private var socket: WebSocket? = null

    // Unlimited buffer so frames arriving between `recv` calls are never lost; a successful
    // close sends a sentinel so the app's recv-loop can terminate cleanly.
    private val incoming = Channel<Pair<Boolean, String>>(Channel.UNLIMITED)

    override suspend fun handle(op: String, input: String): PluginResponse = when (op) {
        "connect" -> connect(input)   // suspends until the socket opens or fails
        "send" -> {
            socket?.send(input)
            if (socket != null) PluginResponse(true, "") else PluginResponse(false, "not connected")
        }
        "recv" -> {
            val (ok, text) = incoming.receive()  // suspends until a frame or close arrives
            PluginResponse(ok, text)
        }
        "close" -> {
            socket?.close(1000, null)
            socket = null
            PluginResponse(true, "")
        }
        else -> PluginResponse(false, "unknown op '$op'")
    }

    private suspend fun connect(url: String): PluginResponse {
        val opened = kotlinx.coroutines.CompletableDeferred<PluginResponse>()
        val request = Request.Builder().url(url).build()
        socket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                if (!opened.isCompleted) opened.complete(PluginResponse(true, ""))
            }
            override fun onMessage(webSocket: WebSocket, text: String) {
                incoming.trySend(true to text)
            }
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                incoming.trySend(false to "closed")
            }
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                if (!opened.isCompleted) opened.complete(PluginResponse(false, t.message ?: "connect failed"))
                incoming.trySend(false to "closed")
            }
        })
        // handle() is a suspend fun, so we can await the open/fail callback directly.
        return opened.await()
    }
}