Refactor KMP networking backends

This commit is contained in:
Sergey Chernov 2026-04-02 20:04:25 +03:00
parent d0aaa2c256
commit cd7e001f41
22 changed files with 706 additions and 851 deletions

View File

@ -1,6 +1,6 @@
### lyng.io.net — TCP and UDP sockets for Lyng scripts ### lyng.io.net — TCP and UDP sockets for Lyng scripts
This module provides minimal raw transport networking for Lyng scripts. It is implemented in `lyngio` and backed by Ktor sockets on the JVM and by Node networking APIs on JS/Node runtimes. This module provides minimal raw transport networking for Lyng scripts. It is implemented in `lyngio` and backed by Ktor sockets on the JVM and Linux Native, and by Node networking APIs on JS/Node runtimes.
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes. > **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
@ -164,4 +164,6 @@ The module uses `NetAccessPolicy` to authorize network operations before they ar
- **Android:** supported via the Ktor CIO and Ktor sockets backends - **Android:** supported via the Ktor CIO and Ktor sockets backends
- **JS/Node:** supported for `resolve`, TCP client/server, and UDP - **JS/Node:** supported for `resolve`, TCP client/server, and UDP
- **JS/browser:** unsupported; capability checks report unavailable - **JS/browser:** unsupported; capability checks report unavailable
- **Other targets:** currently report unsupported; use capability checks before relying on raw sockets - **Linux Native:** supported via Ktor sockets
- **Apple Native:** enabled via the shared native Ktor sockets backend; compile-verified, runtime not yet host-verified
- **Other native targets:** currently report unsupported; use capability checks before relying on raw sockets

View File

@ -86,6 +86,9 @@ kotlin {
} }
val nativeMain by creating { val nativeMain by creating {
dependsOn(commonMain) dependsOn(commonMain)
dependencies {
implementation(libs.ktor.network)
}
} }
val darwinMain by creating { val darwinMain by creating {
dependsOn(nativeMain) dependsOn(nativeMain)
@ -117,6 +120,9 @@ kotlin {
implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlinx.coroutines.test)
} }
} }
val linuxTest by creating {
dependsOn(commonTest)
}
val iosX64Main by getting { dependsOn(iosMain) } val iosX64Main by getting { dependsOn(iosMain) }
val iosArm64Main by getting { dependsOn(iosMain) } val iosArm64Main by getting { dependsOn(iosMain) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain) } val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
@ -124,6 +130,8 @@ kotlin {
val mingwX64Main by getting { dependsOn(mingwMain) } val mingwX64Main by getting { dependsOn(mingwMain) }
val linuxX64Main by getting { dependsOn(linuxMain) } val linuxX64Main by getting { dependsOn(linuxMain) }
val linuxArm64Main by getting { dependsOn(linuxMain) } val linuxArm64Main by getting { dependsOn(linuxMain) }
val linuxX64Test by getting { dependsOn(linuxTest) }
val linuxArm64Test by getting { dependsOn(linuxTest) }
// JS: use runtime detection in jsMain to select Node vs Browser implementation // JS: use runtime detection in jsMain to select Node vs Browser implementation
val jsMain by getting { val jsMain by getting {

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = AndroidKtorLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
private object AndroidKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -1,92 +1,5 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = AndroidKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
private object AndroidKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return AndroidLyngWsSession(url, session)
}
}
private class AndroidLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
@Volatile
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -0,0 +1,61 @@
package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
internal fun createKtorHttpEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
): LyngHttpEngine = KtorLyngHttpEngine(engineFactory)
private class KtorLyngHttpEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
) : LyngHttpEngine {
private val clientResult = runCatching {
HttpClient(engineFactory) {
expectSuccess = false
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -0,0 +1,94 @@
package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
internal fun createKtorWsEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
): LyngWsEngine = KtorLyngWsEngine(engineFactory)
private class KtorLyngWsEngine(
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
) : LyngWsEngine {
private val clientResult = runCatching {
HttpClient(engineFactory) {
install(WebSockets)
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return KtorLyngWsSession(url, session)
}
}
private class KtorLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.darwin.Darwin import io.ktor.client.engine.darwin.Darwin
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = DarwinLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Darwin)
private object DarwinLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Darwin) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -0,0 +1,8 @@
package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true,
isTcpAvailable = true,
isTcpServerAvailable = true,
isUdpAvailable = true,
)

View File

@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.darwin.Darwin import io.ktor.client.engine.darwin.Darwin
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = DarwinKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Darwin)
private object DarwinKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Darwin) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return DarwinLyngWsSession(url, session)
}
}
private class DarwinLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.js.Js import io.ktor.client.engine.js.Js
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = JsKtorLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Js)
private object JsKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Js) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.js.Js import io.ktor.client.engine.js.Js
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = JsKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Js)
private object JsKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Js) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return JsLyngWsSession(url, session)
}
}
private class JsLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = JvmKtorLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
private object JvmKtorLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -17,94 +17,6 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = JvmKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
private object JvmKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(CIO) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return JvmLyngWsSession(url, session)
}
}
private class JvmLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
@Volatile
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
val safeCode = code.toShort()
session.close(CloseReason(safeCode, reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.curl.Curl import io.ktor.client.engine.curl.Curl
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = LinuxLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Curl)
private object LinuxLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Curl) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -0,0 +1,8 @@
package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
isSupported = true,
isTcpAvailable = true,
isTcpServerAvailable = true,
isUdpAvailable = true,
)

View File

@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.curl.Curl import io.ktor.client.engine.curl.Curl
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = LinuxKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Curl)
private object LinuxKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(Curl) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return LinuxLyngWsSession(url, session)
}
}
private class LinuxLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -0,0 +1,90 @@
package net.sergeych.lyngio.net
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.Script
import net.sergeych.lyng.io.net.createNetModule
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class NetLinuxNativeTest {
@Test
fun testLinuxNativeCapabilitiesAndResolve() = runBlocking {
val engine = getSystemNetEngine()
assertTrue(engine.isSupported)
assertTrue(engine.isTcpAvailable)
assertTrue(engine.isTcpServerAvailable)
assertTrue(engine.isUdpAvailable)
val resolved = engine.resolve("127.0.0.1", 4040)
assertEquals(1, resolved.size)
assertEquals("127.0.0.1", resolved.single().host)
assertEquals(4040, resolved.single().port)
assertEquals(LyngIpVersion.IPV4, resolved.single().ipVersion)
assertTrue(resolved.single().resolved)
}
@Test
fun testLinuxNativeLyngModuleCapabilities() = runBlocking {
val scope = Script.newScope()
createNetModule(PermitAllNetAccessPolicy, scope)
val code = """
import lyng.io.net
val a: SocketAddress = Net.resolve("127.0.0.1", 4040)[0]
[Net.isSupported(), Net.isTcpAvailable(), Net.isTcpServerAvailable(), Net.isUdpAvailable(), a.toString(), a.resolved]
""".trimIndent()
val result = Compiler.compile(code).execute(scope).inspect(scope)
assertTrue(result.contains("true,true,true,true"), result)
assertTrue(result.contains("127.0.0.1:4040"), result)
}
@Test
fun testLinuxNativeTcpAndUdpLoopback() = runBlocking {
val engine = getSystemNetEngine()
withTimeout(5_000) {
val server = engine.tcpListen(host = "127.0.0.1", port = 0, backlog = 16, reuseAddress = true)
val accepted = async {
val client = server.accept()
val text = client.read(4)?.decodeToString()
client.writeUtf8("echo:$text")
client.flush()
client.close()
server.close()
text
}
val socket = engine.tcpConnect("127.0.0.1", server.localAddress().port, timeoutMillis = 2_000, noDelay = true)
socket.writeUtf8("ping")
socket.flush()
val reply = socket.read(32)?.decodeToString()
socket.close()
assertEquals("ping", accepted.await())
assertEquals("echo:ping", reply)
}
withTimeout(5_000) {
val receiver = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
val sender = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
sender.send("ping".encodeToByteArray(), "127.0.0.1", receiver.localAddress().port)
val datagram = receiver.receive(32)
sender.close()
receiver.close()
assertEquals("ping", datagram?.data?.decodeToString())
assertTrue((datagram?.address?.port ?: 0) > 0)
}
}
}

View File

@ -1,58 +1,5 @@
package net.sergeych.lyngio.http package net.sergeych.lyngio.http
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.winhttp.WinHttp import io.ktor.client.engine.winhttp.WinHttp
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import io.ktor.http.headers
import io.ktor.http.takeFrom
actual fun getSystemHttpEngine(): LyngHttpEngine = MingwLyngHttpEngine actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(WinHttp)
private object MingwLyngHttpEngine : LyngHttpEngine {
private val clientResult by lazy {
runCatching {
HttpClient(WinHttp) {
expectSuccess = false
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
val httpClient = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
}
val response = httpClient.request {
applyRequest(request)
}
return LyngHttpResponse(
status = response.status.value,
statusText = response.status.description,
headers = response.headers.entries().associate { it.key to it.value.toList() },
bodyBytes = response.body<ByteArray>(),
)
}
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
method = HttpMethod.parse(request.method.uppercase())
url.takeFrom(request.url)
headers {
request.headers.forEach { (name, value) -> append(name, value) }
}
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
when {
request.bodyBytes != null && request.bodyText != null ->
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
request.bodyBytes != null -> setBody(request.bodyBytes)
request.bodyText != null -> setBody(request.bodyText)
}
}
}

View File

@ -1,91 +1,5 @@
package net.sergeych.lyngio.ws package net.sergeych.lyngio.ws
import io.ktor.client.HttpClient
import io.ktor.client.engine.winhttp.WinHttp import io.ktor.client.engine.winhttp.WinHttp
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.websocket.CloseReason
import io.ktor.websocket.DefaultWebSocketSession
import io.ktor.websocket.Frame
import io.ktor.websocket.close
import io.ktor.websocket.readText
import io.ktor.websocket.send
import kotlinx.coroutines.channels.ClosedReceiveChannelException
actual fun getSystemWsEngine(): LyngWsEngine = MingwKtorWsEngine actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(WinHttp)
private object MingwKtorWsEngine : LyngWsEngine {
private val clientResult by lazy {
runCatching {
HttpClient(WinHttp) {
install(WebSockets)
}
}
}
override val isSupported: Boolean
get() = clientResult.isSuccess
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
val client = clientResult.getOrElse {
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
}
val session = client.webSocketSession {
url(url)
headers.forEach { (name, value) -> header(name, value) }
}
return MingwLyngWsSession(url, session)
}
}
private class MingwLyngWsSession(
private val targetUrl: String,
private val session: DefaultWebSocketSession,
) : LyngWsSession {
private var closed = false
override fun isOpen(): Boolean = !closed
override fun url(): String = targetUrl
override suspend fun sendText(text: String) {
ensureOpen()
session.send(text)
}
override suspend fun sendBytes(data: ByteArray) {
ensureOpen()
session.send(data)
}
override suspend fun receive(): LyngWsMessage? {
if (closed) return null
val frame = try {
session.incoming.receive()
} catch (_: ClosedReceiveChannelException) {
closed = true
return null
}
return when (frame) {
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
is Frame.Close -> {
closed = true
null
}
else -> receive()
}
}
override suspend fun close(code: Int, reason: String) {
if (closed) return
closed = true
session.close(CloseReason(code.toShort(), reason))
}
private fun ensureOpen() {
if (closed) throw IllegalStateException("websocket session is closed")
}
}

View File

@ -0,0 +1,216 @@
package net.sergeych.lyngio.net
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.BoundDatagramSocket
import io.ktor.network.sockets.Datagram
import io.ktor.network.sockets.InetSocketAddress
import io.ktor.network.sockets.ServerSocket
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.SocketAddress
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.isClosed
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.readAvailable
import io.ktor.utils.io.readUTF8Line
import io.ktor.utils.io.writeFully
import io.ktor.utils.io.writeStringUtf8
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withTimeout
import kotlinx.io.Buffer
import kotlinx.io.readByteArray
internal fun createNativeKtorNetEngine(
isSupported: Boolean,
isTcpAvailable: Boolean,
isTcpServerAvailable: Boolean,
isUdpAvailable: Boolean,
): LyngNetEngine = NativeKtorNetEngine(
isSupported = isSupported,
isTcpAvailable = isTcpAvailable,
isTcpServerAvailable = isTcpServerAvailable,
isUdpAvailable = isUdpAvailable,
)
private class NativeKtorNetEngine(
override val isSupported: Boolean,
override val isTcpAvailable: Boolean,
override val isTcpServerAvailable: Boolean,
override val isUdpAvailable: Boolean,
) : LyngNetEngine {
private val selectorManager: SelectorManager by lazy { SelectorManager(Dispatchers.Default) }
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> {
val rawAddress = InetSocketAddress(host, port).resolveAddress()
?: throw IllegalStateException("Failed to resolve address for $host")
return listOf(
LyngSocketAddress(
host = rawAddress.toIpHostString(),
port = port,
ipVersion = rawAddress.toLyngIpVersion(),
resolved = true,
)
)
}
override suspend fun tcpConnect(
host: String,
port: Int,
timeoutMillis: Long?,
noDelay: Boolean,
): LyngTcpSocket {
val connectBlock: suspend () -> Socket = {
aSocket(selectorManager).tcp().connect(host, port) {
this.noDelay = noDelay
}
}
val socket = if (timeoutMillis != null) withTimeout(timeoutMillis) { connectBlock() } else connectBlock()
return NativeLyngTcpSocket(socket)
}
override suspend fun tcpListen(
host: String?,
port: Int,
backlog: Int,
reuseAddress: Boolean,
): LyngTcpServer {
val bindHost = host ?: "0.0.0.0"
val server = aSocket(selectorManager).tcp().bind(bindHost, port) {
backlogSize = backlog
this.reuseAddress = reuseAddress
}
return NativeLyngTcpServer(server)
}
override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket {
val bindHost = host ?: "0.0.0.0"
val socket = aSocket(selectorManager).udp().bind(bindHost, port) {
this.reuseAddress = reuseAddress
}
return NativeLyngUdpSocket(socket)
}
}
private class NativeLyngTcpSocket(
private val socket: Socket,
) : LyngTcpSocket {
private val input: ByteReadChannel by lazy { socket.openReadChannel() }
private val output: ByteWriteChannel by lazy { socket.openWriteChannel(autoFlush = true) }
override fun isOpen(): Boolean = !socket.isClosed
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
override fun remoteAddress(): LyngSocketAddress = socket.remoteAddress.toLyngSocketAddress(resolved = true)
override suspend fun read(maxBytes: Int): ByteArray? {
if (!input.awaitContent(1)) return null
val buffer = ByteArray(maxBytes)
val count = input.readAvailable(buffer, 0, maxBytes)
return when {
count <= 0 -> null
count == maxBytes -> buffer
else -> buffer.copyOf(count)
}
}
override suspend fun readLine(): String? = input.readUTF8Line()
override suspend fun write(data: ByteArray) {
output.writeFully(data, 0, data.size)
}
override suspend fun writeUtf8(text: String) {
output.writeStringUtf8(text)
}
override suspend fun flush() {
output.flush()
}
override fun close() {
socket.close()
}
}
private class NativeLyngTcpServer(
private val server: ServerSocket,
) : LyngTcpServer {
override fun isOpen(): Boolean = !server.isClosed
override fun localAddress(): LyngSocketAddress = server.localAddress.toLyngSocketAddress(resolved = true)
override suspend fun accept(): LyngTcpSocket = NativeLyngTcpSocket(server.accept())
override fun close() {
server.close()
}
}
private class NativeLyngUdpSocket(
private val socket: BoundDatagramSocket,
) : LyngUdpSocket {
override fun isOpen(): Boolean = !socket.isClosed
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
override suspend fun receive(maxBytes: Int): LyngDatagram? {
val datagram = try {
socket.receive()
} catch (e: Throwable) {
if (!isOpen()) return null
throw e
}
val bytes = datagram.packet.readByteArray().let {
if (it.size <= maxBytes) it else it.copyOf(maxBytes)
}
return LyngDatagram(bytes, datagram.address.toLyngSocketAddress(resolved = true))
}
override suspend fun send(data: ByteArray, host: String, port: Int) {
val packet = Buffer()
packet.write(data)
socket.send(Datagram(packet, InetSocketAddress(host, port)))
}
override fun close() {
socket.close()
}
}
private fun SocketAddress.toLyngSocketAddress(resolved: Boolean): LyngSocketAddress {
val inetAddress = this as? InetSocketAddress
if (inetAddress != null) {
val rawAddress = inetAddress.resolveAddress()
val host = rawAddress?.toIpHostString() ?: inetAddress.hostname
return LyngSocketAddress(
host = host,
port = inetAddress.port,
ipVersion = rawAddress?.toLyngIpVersion()
?: if (host.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
resolved = resolved,
)
}
val rendered = toString()
return LyngSocketAddress(
host = rendered,
port = 0,
ipVersion = if (rendered.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
resolved = resolved,
)
}
private fun ByteArray.toLyngIpVersion(): LyngIpVersion = if (size == 16) LyngIpVersion.IPV6 else LyngIpVersion.IPV4
private fun ByteArray.toIpHostString(): String = when (size) {
4 -> joinToString(".") { (it.toInt() and 0xff).toString() }
16 -> (0 until 8).joinToString(":") { index ->
val hi = this[index * 2].toInt() and 0xff
val lo = this[index * 2 + 1].toInt() and 0xff
((hi shl 8) or lo).toString(16)
}
else -> error("Unsupported IP address length: $size")
}

View File

@ -0,0 +1,205 @@
# Networking Handoff
Date: 2026-04-02
Commit: `5346d15` (`Add KMP networking backends`)
## Scope completed
The `lyngio` networking work now provides a uniform Lyng-facing API with capability probes and platform-specific implementations.
Implemented modules:
- `lyng.io.http`
- `lyng.io.ws`
- `lyng.io.net`
## Current support matrix
### HTTP / HTTPS
- JVM: supported
- Android: supported
- JS: supported
- Linux Native: supported
- Windows Native (`mingwX64`): supported
- Apple Native: compile-verified on this Linux host
### WS / WSS
- JVM: supported
- Android: supported
- JS: supported
- Linux Native: supported
- Windows Native (`mingwX64`): supported
- Apple Native: compile-verified on this Linux host
### Raw networking (`lyng.io.net`)
- JVM: supported
- Android: supported
- JS/Node: supported
- JS/browser: unsupported by capability probe
- Linux Native: supported
- Apple Native: enabled via shared native backend; compile-verified, runtime not yet host-verified
- Other Native targets: intentionally still unsupported
## Important design decisions
- Ktor is the backend for all currently implemented networking.
- API is uniform across targets; platform variance is exposed through capability checks such as:
- `Http.isSupported()`
- `Ws.isSupported()`
- `Net.isSupported()`
- `Net.isTcpAvailable()`
- `Net.isTcpServerAvailable()`
- `Net.isUdpAvailable()`
- Native support was restricted to what matches Ktor client-engine support:
- Darwin for Apple Native
- Curl for Linux Native
- WinHttp for Windows Native
- Native raw sockets use a shared Ktor socket implementation for Linux and Darwin source sets.
- Capability probes are enabled on Linux Native and Apple Native; Apple Native is compile-verified but not yet runtime-tested on a macOS host.
## Documentation and tests status
Docs updated:
- `docs/lyng.io.http.md`
- `docs/lyng.io.ws.md`
- `docs/lyng.io.net.md`
Verified docs/tests:
- HTTP/HTTPS docs are covered with extracted markdown tests on JVM.
- WS/WSS docs are covered with extracted markdown tests on JVM.
- JS/Node has both engine-level and Lyng-module-level tests for raw networking.
## Key implementation files
Shared:
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/http/LyngHttp.kt`
- `lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/LyngHttpModule.kt`
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/ws/LyngWs.kt`
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt`
- `lyngio/build.gradle.kts`
- `gradle/libs.versions.toml`
Platform HTTP:
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/http/PlatformJvm.kt`
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/http/PlatformJs.kt`
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/http/PlatformAndroid.kt`
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/http/PlatformDarwin.kt`
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/http/PlatformLinux.kt`
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/http/PlatformMingw.kt`
Platform WS:
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/ws/PlatformJvm.kt`
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/ws/PlatformJs.kt`
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/ws/PlatformAndroid.kt`
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/ws/PlatformDarwin.kt`
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/ws/PlatformLinux.kt`
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/ws/PlatformMingw.kt`
Platform raw net:
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt`
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt`
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt`
- `lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt`
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt`
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt`
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt`
JS tests:
- `lyngio/src/jsTest/kotlin/net/sergeych/lyngio/PlatformCapabilityJsTest.kt`
- `lyngio/src/jsTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt`
- `lyngio/src/jsTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt`
JVM tests:
- `lyngio/src/jvmTest/kotlin/LyngioBookTest.kt`
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/LyngHttpModuleTest.kt`
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/ws/LyngWsModuleTest.kt`
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleTest.kt`
Linux Native tests:
- `lyngio/src/linuxTest/kotlin/net/sergeych/lyngio/net/NetLinuxNativeTest.kt`
## Verification already run
JVM / docs:
- `./gradlew :lyngio:jvmTest --tests LyngioBookTest`
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.http.LyngHttpModuleTest`
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.ws.LyngWsModuleTest`
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.net.LyngNetModuleTest`
JS:
- `./gradlew :lyngio:compileKotlinJs`
- `./gradlew :lyngio:compileTestKotlinJs`
- `./gradlew :lyngio:jsNodeTest`
- `./gradlew kotlinUpgradeYarnLock`
Android:
- `./gradlew :lyngio:compileDebugKotlinAndroid`
- `./gradlew :lyngio:compileReleaseKotlinAndroid`
Native:
- `./gradlew :lyngio:compileKotlinLinuxX64`
- `./gradlew :lyngio:compileKotlinLinuxArm64`
- `./gradlew :lyngio:compileKotlinMingwX64`
- `./gradlew :lyngio:compileKotlinIosX64`
- `./gradlew :lyngio:compileKotlinIosArm64`
- `./gradlew :lyngio:compileKotlinIosSimulatorArm64`
- `./gradlew :lyngio:compileKotlinMacosArm64`
- `./gradlew :lyngio:compileTestKotlinLinuxX64`
- `./gradlew :lyngio:compileTestKotlinLinuxArm64`
- `./gradlew :lyngio:linkDebugTestLinuxX64`
- `./gradlew :lyngio:linuxX64Test`
- `./gradlew :lyngio:linuxX64Test --tests net.sergeych.lyngio.net.NetLinuxNativeTest`
- `./gradlew :lyngio:linuxX64Test --tests net.sergeych.lyngio.net.NetLinuxNativeTest.testLinuxNativeTcpAndUdpLoopback`
- `./lyngio/build/bin/linuxX64/debugTest/test.kexe --ktest_filter='net.sergeych.lyngio.net.NetLinuxNativeTest.*'`
## Known intentional gaps
- Native raw sockets are enabled on Linux Native and Apple Native.
- Apple Native raw networking is enabled based on shared-backend compile verification; runtime verification on macOS is still pending.
- No Android device/instrumented runtime tests were added; only compile verification was done.
## Worktree state after commit
Current HEAD:
- `5346d15` `Add KMP networking backends`
Unrelated remaining change:
- `examples/tetris_console.lyng`
That file was not touched by the networking work and was intentionally left out of the commit.
## Recommended next steps
1. Add Native raw socket support only per target that compiles and passes a smoke test.
2. Keep capability probes `false` on non-Linux Native targets until each raw-socket backend is proven.
3. If Apple Native work continues, compile Darwin targets on a macOS host before claiming support.
4. Run a macOS-hosted runtime smoke test when available to verify the already-enabled Darwin backend.
5. Optionally add Android runtime tests later; compile-only verification exists now.
## Suggested first task for the next chat
Continue Native raw `lyng.io.net` incrementally from the verified Linux baseline:
- keep Linux Native enabled
- keep Apple Native enabled unless runtime verification disproves the shared-backend assumption
- keep other Native targets capability-gated off until compiled and smoke-tested
- use only `ktor-network` support that actually compiles
- do not change the Lyng-facing API