7.5 KiB
lyng.io.ws - WebSocket client for Lyng scripts
This module provides a compact WebSocket client API for Lyng scripts. It is implemented in lyngio and currently backed by Ktor WebSockets on the JVM.
Note:
lyngiois a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.Shared type note:
WsMessageis also available fromlyng.io.ws.typeswhen host code wants the reusable message type without depending on the WebSocket client module itself.
Install The Module Into A Lyng Session
Kotlin host bootstrap example:
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.Scope
import net.sergeych.lyng.io.ws.createWsModule
import net.sergeych.lyngio.ws.security.PermitAllWsAccessPolicy
suspend fun bootstrapWs() {
val session = EvalSession()
val scope: Scope = session.getScope()
createWsModule(PermitAllWsAccessPolicy, scope)
session.eval("import lyng.io.ws")
}
Using From Lyng Scripts
Text Exchange
import lyng.io.ws
val ws = Ws.connect(WS_TEST_URL)
ws.sendText("ping")
val m: WsMessage = ws.receive()
ws.close()
[ws.url() == WS_TEST_URL, m.isText, m.text]
>>> [true,true,echo:ping]
Binary Exchange
import lyng.buffer
import lyng.io.ws
val ws = Ws.connect(WS_TEST_BINARY_URL)
ws.sendBytes(Buffer(9, 8, 7))
val m: WsMessage = ws.receive()
ws.close()
[m.isText, (m.data as Buffer).hex]
>>> [false,010203090807]
Secure wss Exchange
import lyng.io.ws
val ws = Ws.connect(WSS_TEST_URL)
ws.sendText("ping")
val m: WsMessage = ws.receive()
ws.close()
[ws.url() == WSS_TEST_URL, m.text]
>>> [true,secure:ping]
Message Flow And Session Lifecycle
Reading Incoming Messages
Call ws.receive() to wait for the next application message.
What receive() returns:
WsMessagefor the next text or binary message.nullafter the peer closes the connection cleanly.nullafter the transport has already been closed and no more messages can arrive.
What reaches Lyng code:
- Text frames are exposed as
WsMessage(isText = true, text = ...). - Binary frames are exposed as
WsMessage(isText = false, data = ...). - Fragmented websocket messages are reassembled before they are returned.
- Ping and pong control frames are handled internally and are not returned by
receive(). - Incoming close frames are handled internally; after that
receive()returnsnull.
Typical receive loop:
import lyng.buffer
import lyng.io.ws
val ws = Ws.connect(WS_URL)
while (true) {
val msg = ws.receive() ?: break
if (msg.isText) {
println("text=" + msg.text)
} else {
println("bytes=" + ((msg.data as Buffer).size))
}
}
println("peer closed the websocket")
Sending Outgoing Messages
Use:
ws.sendText(text)for UTF-8 text messages.ws.sendBytes(data)for binary messages.
Example:
import lyng.buffer
import lyng.io.ws
val ws = Ws.connect(WS_URL)
ws.sendText("hello")
ws.sendBytes(Buffer(1, 2, 3, 4))
Send behavior:
- Each call sends one websocket message.
- The API does not expose partial-frame streaming; send the whole message in one call.
- If the session is already closed,
sendText(...)andsendBytes(...)fail with a websocket error. - If the transport breaks during send, the session is released and the send call fails.
Detecting Closed Connections
Use both signals together:
ws.isOpen()tells you whether the session is still considered open right now.ws.receive() == nulltells you the receive side has reached the end of the websocket session.
Practical rule:
- If
receive()returnsnull, stop reading and treat the session as closed. - After close has been observed, do not attempt further sends.
The API does not currently expose the peer close code or close reason to Lyng code.
Closing The Connection Yourself
Call ws.close() when you are done.
import lyng.io.ws
val ws = Ws.connect(WS_URL)
ws.sendText("bye")
ws.close(1000, "done")
Close semantics:
close()sends a websocket close frame with the given code and reason.- Defaults are
code = 1000andreason = "". - After
close(), the session is released locally and should be treated as closed immediately. - Calling
close()on an already closed session is a no-op. - After local close,
receive()returnsnulland further sends fail.
Recommended Usage Pattern
For request-response style exchanges:
import lyng.io.ws
val ws = Ws.connect(WS_URL)
try {
ws.sendText("ping")
val reply = ws.receive() ?: error("socket closed before reply")
println(reply.text)
} finally {
ws.close()
}
For long-lived consumers:
import lyng.io.ws
val ws = Ws.connect(WS_URL)
try {
while (true) {
val msg = ws.receive() ?: break
if (msg.isText) {
println(msg.text)
}
}
} finally {
ws.close()
}
API Reference
Ws
isSupported(): Bool- whether WebSocket client support is available on the current runtime.connect(url: String, headers...): WsSession- open a client websocket session.
headers... accepts:
MapEntry, for example"Authorization" => "Bearer x"- 2-item lists, for example
["Authorization", "Bearer x"]
WsSession
isOpen(): Boolurl(): StringsendText(text: String): voidsendBytes(data: Buffer): voidreceive(): WsMessage?close(code: Int = 1000, reason: String = ""): void
Behavior summary:
receive()returnsnullafter close.close()is safe to call more than once.- send operations require an open session.
WsMessage
isText: Booltext: String?data: Buffer?
Payload rules:
- Text messages populate
textand leavedata == null. - Binary messages populate
dataand leavetext == null.
Security Policy
The module uses WsAccessPolicy to authorize websocket operations.
WsAccessPolicy- interface for custom policies.PermitAllWsAccessPolicy- allows all websocket operations.WsAccessOp.Connect(url)WsAccessOp.Send(url, bytes, isText)WsAccessOp.Receive(url)
Example restricted policy in Kotlin:
import net.sergeych.lyngio.fs.security.AccessContext
import net.sergeych.lyngio.fs.security.AccessDecision
import net.sergeych.lyngio.fs.security.Decision
import net.sergeych.lyngio.ws.security.WsAccessOp
import net.sergeych.lyngio.ws.security.WsAccessPolicy
val allowLocalOnly = object : WsAccessPolicy {
override suspend fun check(op: WsAccessOp, ctx: AccessContext): AccessDecision =
when (op) {
is WsAccessOp.Connect ->
if (
op.url.startsWith("ws://127.0.0.1:") ||
op.url.startsWith("wss://127.0.0.1:") ||
op.url.startsWith("ws://localhost:") ||
op.url.startsWith("wss://localhost:")
)
AccessDecision(Decision.Allow)
else
AccessDecision(Decision.Deny, "only local ws/wss connections are allowed")
else -> AccessDecision(Decision.Allow)
}
}
Platform Support
- JVM: supported.
- Android: supported via the Ktor CIO websocket client backend.
- JS: supported via the Ktor JS websocket client backend.
- Linux native: supported via the Ktor Curl websocket client backend.
- Windows native: supported via the Ktor WinHttp websocket client backend.
- Apple native: supported via the Ktor Darwin websocket client backend.
- Other targets: may report unsupported; use
Ws.isSupported()before relying on websocket client access.