added nullable ?: break support with proper inference ;)
This commit is contained in:
parent
fae9965bdf
commit
79429d5f2d
10
docs/ai_notes_docs_headings.md
Normal file
10
docs/ai_notes_docs_headings.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# AI notes: heading levels must be consecutive
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
|
When editing repository documentation:
|
||||||
|
|
||||||
|
- Use heading levels in order: `#`, then `##`, then `###`, and so on.
|
||||||
|
- Do not skip levels, for example `#` directly to `###`.
|
||||||
|
- Keep the heading tree balanced inside each document; sibling sections should use the same level.
|
||||||
|
- If you add a subsection and the parent is `##`, the child must be `###`.
|
||||||
@ -1,9 +1,8 @@
|
|||||||
### lyng.io.http.server — Minimal HTTP/1.1 and WebSocket server
|
# `lyng.io.http.server` - Minimal HTTP/1.1 And WebSocket Server
|
||||||
|
|
||||||
This module provides a small server-side HTTP API for Lyng scripts. It is implemented in `lyngio` on top of the existing TCP layer and is intended for embedded tools, local services, test fixtures, and lightweight app backends.
|
This module provides a small server-side HTTP API for Lyng scripts. It is implemented in `lyngio` on top of the existing TCP layer and is intended for embedded tools, local services, test fixtures, and lightweight app backends.
|
||||||
|
|
||||||
It supports:
|
It supports:
|
||||||
|
|
||||||
- HTTP/1.1 request parsing
|
- HTTP/1.1 request parsing
|
||||||
- keep-alive
|
- keep-alive
|
||||||
- exact-path routing
|
- exact-path routing
|
||||||
@ -15,9 +14,7 @@ It does not aim to replace a full reverse proxy. Typical deployment is behind ng
|
|||||||
|
|
||||||
> **Security note:** this module uses the same `NetAccessPolicy` capability model as raw TCP sockets. If scripts are allowed to listen on TCP, they can host an HTTP server.
|
> **Security note:** this module uses the same `NetAccessPolicy` capability model as raw TCP sockets. If scripts are allowed to listen on TCP, they can host an HTTP server.
|
||||||
|
|
||||||
---
|
## Install The Module Into A Lyng Session
|
||||||
|
|
||||||
#### Install the module into a Lyng session
|
|
||||||
|
|
||||||
Kotlin bootstrap example:
|
Kotlin bootstrap example:
|
||||||
|
|
||||||
@ -35,9 +32,7 @@ suspend fun bootstrapHttpServer() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## RequestContext Sugar
|
||||||
|
|
||||||
#### RequestContext Sugar
|
|
||||||
|
|
||||||
Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as:
|
Route handlers use `RequestContext` as the receiver, so inside handlers you normally write direct calls such as:
|
||||||
|
|
||||||
@ -48,11 +43,9 @@ Route handlers use `RequestContext` as the receiver, so inside handlers you norm
|
|||||||
- `request.path`
|
- `request.path`
|
||||||
- `routeParams["id"]`
|
- `routeParams["id"]`
|
||||||
|
|
||||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request/exchange parameter through every route lambda.
|
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda.
|
||||||
|
|
||||||
---
|
## JSON API Sugar
|
||||||
|
|
||||||
#### JSON API Sugar
|
|
||||||
|
|
||||||
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
||||||
|
|
||||||
@ -61,7 +54,7 @@ For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
|||||||
|
|
||||||
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
||||||
|
|
||||||
**Typed JSON POST**
|
### Typed JSON POST
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
import lyng.io.http.server
|
import lyng.io.http.server
|
||||||
@ -85,7 +78,7 @@ server.postPath("/api/users") {
|
|||||||
server.listen(8080, "127.0.0.1")
|
server.listen(8080, "127.0.0.1")
|
||||||
```
|
```
|
||||||
|
|
||||||
**JSON response with route params**
|
### JSON Response With Route Params
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
import lyng.io.http.server
|
import lyng.io.http.server
|
||||||
@ -103,9 +96,7 @@ server.getPath("/api/users/{id}") {
|
|||||||
server.listen(8080, "127.0.0.1")
|
server.listen(8080, "127.0.0.1")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Request And Route Data
|
||||||
|
|
||||||
#### Request and Route Data
|
|
||||||
|
|
||||||
`ServerRequest` exposes parsed HTTP request data:
|
`ServerRequest` exposes parsed HTTP request data:
|
||||||
|
|
||||||
@ -135,9 +126,7 @@ For exact routes, `routeMatch` is `null` and `routeParams` is empty.
|
|||||||
For regex routes, `routeMatch` is set and `routeParams` is empty.
|
For regex routes, `routeMatch` is set and `routeParams` is empty.
|
||||||
For path-template routes, both `routeMatch` and `routeParams` are set.
|
For path-template routes, both `routeMatch` and `routeParams` are set.
|
||||||
|
|
||||||
---
|
## Reusable Routers
|
||||||
|
|
||||||
#### Reusable Routers
|
|
||||||
|
|
||||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
||||||
Mount it into `HttpServer` or another `Router`.
|
Mount it into `HttpServer` or another `Router`.
|
||||||
@ -164,11 +153,9 @@ server.listen(8080, "127.0.0.1")
|
|||||||
|
|
||||||
Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer.
|
Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer.
|
||||||
|
|
||||||
---
|
## WebSocket Routes
|
||||||
|
|
||||||
#### WebSocket Routes
|
You can route websocket upgrades by exact path, regex, or path template.
|
||||||
|
|
||||||
You can route websocket upgrades by exact path, regex, or path template:
|
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
server.ws("/chat") { ws ->
|
server.ws("/chat") { ws ->
|
||||||
@ -182,9 +169,147 @@ server.wsPath("/ws/{room}") { ws ->
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
A websocket handler runs only for requests that actually ask for websocket upgrade. Ordinary HTTP requests to the same path are not treated as websocket sessions.
|
||||||
|
|
||||||
#### Path-Template Routes
|
### Choosing Between `ws(...)` And `acceptWebSocket(...)`
|
||||||
|
|
||||||
|
Use `server.ws(...)` or `server.wsPath(...)` when the route is always a websocket endpoint.
|
||||||
|
|
||||||
|
Use `acceptWebSocket(...)` inside a normal HTTP handler when the same route may inspect the request first and then decide whether to upgrade.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
server.get("/maybe-upgrade") {
|
||||||
|
if (!request.isWebSocketUpgrade()) {
|
||||||
|
respondText(400, "websocket upgrade required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptWebSocket { ws ->
|
||||||
|
ws.sendText("connected")
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading Incoming Messages
|
||||||
|
|
||||||
|
Inside a websocket handler, call `ws.receive()` to wait for the next application message.
|
||||||
|
|
||||||
|
What `receive()` returns:
|
||||||
|
- `WsMessage` for the next text or binary message.
|
||||||
|
- `null` after the client sends a close frame.
|
||||||
|
- `null` after the socket is already closed and no more frames can arrive.
|
||||||
|
|
||||||
|
What reaches Lyng code:
|
||||||
|
- Text frames become `WsMessage(isText = true, text = ...)`.
|
||||||
|
- Binary frames become `WsMessage(isText = false, data = ...)`.
|
||||||
|
- Fragmented websocket messages are reassembled before they are returned.
|
||||||
|
- Ping and pong control frames are handled internally and do not appear in Lyng.
|
||||||
|
- A client close frame is answered by the server close handshake, then `receive()` returns `null`.
|
||||||
|
|
||||||
|
Typical server receive loop:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.buffer
|
||||||
|
|
||||||
|
server.ws("/echo") { ws ->
|
||||||
|
while (true) {
|
||||||
|
val msg = ws.receive() ?: break
|
||||||
|
if (msg.isText) {
|
||||||
|
ws.sendText("echo:" + msg.text)
|
||||||
|
} else {
|
||||||
|
ws.sendBytes(msg.data as Buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending Outgoing Messages
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- `ws.sendText(text)` for text messages.
|
||||||
|
- `ws.sendBytes(data)` for binary messages.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.buffer
|
||||||
|
|
||||||
|
server.ws("/push") { ws ->
|
||||||
|
ws.sendText("ready")
|
||||||
|
ws.sendBytes(Buffer(1, 2, 3))
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Send behavior:
|
||||||
|
- Each call sends one websocket message.
|
||||||
|
- The server API does not expose frame-by-frame streaming.
|
||||||
|
- Once the session is closed, send calls fail with a websocket error.
|
||||||
|
|
||||||
|
### What Happens When The Connection Closes
|
||||||
|
|
||||||
|
There are three practical cases:
|
||||||
|
|
||||||
|
1. The client closes first.
|
||||||
|
The runtime replies with a close frame, releases the socket, and `receive()` returns `null`.
|
||||||
|
|
||||||
|
2. Your handler closes first with `ws.close(...)`.
|
||||||
|
The runtime sends a close frame and releases the socket locally.
|
||||||
|
|
||||||
|
3. The transport disappears unexpectedly.
|
||||||
|
The session is released and no more messages can be received; subsequent sends fail.
|
||||||
|
|
||||||
|
What Lyng code should do:
|
||||||
|
- Treat `receive() == null` as end-of-session.
|
||||||
|
- Exit the handler or break the receive loop at that point.
|
||||||
|
- Do not keep sending after close has been observed.
|
||||||
|
|
||||||
|
The current server-side API does not expose the peer close code or close reason to Lyng.
|
||||||
|
|
||||||
|
### Closing The Connection Yourself
|
||||||
|
|
||||||
|
Call `ws.close()` when you want to terminate the websocket session.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
server.ws("/chat") { ws ->
|
||||||
|
ws.sendText("server shutting down")
|
||||||
|
ws.close(1000, "done")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Close semantics:
|
||||||
|
- `close()` sends a websocket close frame with the given code and reason.
|
||||||
|
- Defaults are `code = 1000` and `reason = ""`.
|
||||||
|
- `close()` is idempotent; calling it again after close does nothing.
|
||||||
|
- After local close, the session should be treated as unusable.
|
||||||
|
- After close, `isOpen()` becomes false and further sends fail.
|
||||||
|
|
||||||
|
### WebSocket Handler Pattern
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.http.server
|
||||||
|
|
||||||
|
val server = HttpServer()
|
||||||
|
|
||||||
|
server.wsPath("/rooms/{room}") { ws ->
|
||||||
|
val room = routeParams["room"] ?: "<unknown>"
|
||||||
|
ws.sendText("joined:" + room)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val msg = ws.receive() ?: break
|
||||||
|
if (msg.isText) {
|
||||||
|
ws.sendText(room + ":" + msg.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
server.listen(8080, "127.0.0.1")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path-Template Routes
|
||||||
|
|
||||||
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`.
|
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`.
|
||||||
|
|
||||||
@ -198,7 +323,6 @@ server.getPath("/users/{userId}/posts/{postId}") {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Template rules:
|
Template rules:
|
||||||
|
|
||||||
- template must start with `/`
|
- template must start with `/`
|
||||||
- a segment is either literal text or `{name}`
|
- a segment is either literal text or `{name}`
|
||||||
- parameter names must be valid identifiers
|
- parameter names must be valid identifiers
|
||||||
@ -208,9 +332,7 @@ Template rules:
|
|||||||
- `+` stays `+`
|
- `+` stays `+`
|
||||||
- malformed `%` stays literal
|
- malformed `%` stays literal
|
||||||
|
|
||||||
---
|
## Regex Routes
|
||||||
|
|
||||||
#### Regex Routes
|
|
||||||
|
|
||||||
Regex routes match the whole request path, not a substring.
|
Regex routes match the whole request path, not a substring.
|
||||||
|
|
||||||
@ -221,9 +343,7 @@ server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Basic Exact Route
|
||||||
|
|
||||||
#### Basic Exact Route
|
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
import lyng.io.http.server
|
import lyng.io.http.server
|
||||||
@ -236,9 +356,7 @@ server.get("/hello") {
|
|||||||
server.listen(8080, "127.0.0.1")
|
server.listen(8080, "127.0.0.1")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Route Precedence
|
||||||
|
|
||||||
#### Route Precedence
|
|
||||||
|
|
||||||
Dispatch order is:
|
Dispatch order is:
|
||||||
|
|
||||||
@ -250,11 +368,9 @@ Dispatch order is:
|
|||||||
|
|
||||||
This means exact routes stay fast and always win over template or regex routes for the same path.
|
This means exact routes stay fast and always win over template or regex routes for the same path.
|
||||||
|
|
||||||
---
|
## API Surface
|
||||||
|
|
||||||
#### API Surface
|
### `Router` Route Registration Methods
|
||||||
|
|
||||||
`Router` route registration methods:
|
|
||||||
|
|
||||||
- `get(path: String|Regex, handler)`
|
- `get(path: String|Regex, handler)`
|
||||||
- `getPath(pathTemplate: String, handler)`
|
- `getPath(pathTemplate: String, handler)`
|
||||||
@ -271,7 +387,7 @@ This means exact routes stay fast and always win over template or regex routes f
|
|||||||
- `fallback(handler)`
|
- `fallback(handler)`
|
||||||
- `mount(router)`
|
- `mount(router)`
|
||||||
|
|
||||||
`HttpServer` route registration methods:
|
### `HttpServer` Route Registration Methods
|
||||||
|
|
||||||
- `get(path: String|Regex, handler)`
|
- `get(path: String|Regex, handler)`
|
||||||
- `getPath(pathTemplate: String, handler)`
|
- `getPath(pathTemplate: String, handler)`
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
### lyng.io.ws — WebSocket client for Lyng scripts
|
# `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.
|
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.
|
||||||
|
|
||||||
@ -6,11 +6,9 @@ This module provides a compact WebSocket client API for Lyng scripts. It is impl
|
|||||||
>
|
>
|
||||||
> **Shared type note:** `WsMessage` is also available from `lyng.io.ws.types` when host code wants the reusable message type without depending on the WebSocket client module itself.
|
> **Shared type note:** `WsMessage` is also available from `lyng.io.ws.types` when host code wants the reusable message type without depending on the WebSocket client module itself.
|
||||||
|
|
||||||
---
|
## Install The Module Into A Lyng Session
|
||||||
|
|
||||||
#### Install the module into a Lyng session
|
Kotlin host bootstrap example:
|
||||||
|
|
||||||
Kotlin (host) bootstrap example:
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.EvalSession
|
import net.sergeych.lyng.EvalSession
|
||||||
@ -26,12 +24,11 @@ suspend fun bootstrapWs() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Using From Lyng Scripts
|
||||||
|
|
||||||
#### Using from Lyng scripts
|
### Text Exchange
|
||||||
|
|
||||||
Simple text message exchange:
|
|
||||||
|
|
||||||
|
```lyng
|
||||||
import lyng.io.ws
|
import lyng.io.ws
|
||||||
|
|
||||||
val ws = Ws.connect(WS_TEST_URL)
|
val ws = Ws.connect(WS_TEST_URL)
|
||||||
@ -40,9 +37,11 @@ Simple text message exchange:
|
|||||||
ws.close()
|
ws.close()
|
||||||
[ws.url() == WS_TEST_URL, m.isText, m.text]
|
[ws.url() == WS_TEST_URL, m.isText, m.text]
|
||||||
>>> [true,true,echo:ping]
|
>>> [true,true,echo:ping]
|
||||||
|
```
|
||||||
|
|
||||||
Binary message exchange:
|
### Binary Exchange
|
||||||
|
|
||||||
|
```lyng
|
||||||
import lyng.buffer
|
import lyng.buffer
|
||||||
import lyng.io.ws
|
import lyng.io.ws
|
||||||
|
|
||||||
@ -52,9 +51,11 @@ Binary message exchange:
|
|||||||
ws.close()
|
ws.close()
|
||||||
[m.isText, (m.data as Buffer).hex]
|
[m.isText, (m.data as Buffer).hex]
|
||||||
>>> [false,010203090807]
|
>>> [false,010203090807]
|
||||||
|
```
|
||||||
|
|
||||||
Secure websocket (`wss`) exchange:
|
### Secure `wss` Exchange
|
||||||
|
|
||||||
|
```lyng
|
||||||
import lyng.io.ws
|
import lyng.io.ws
|
||||||
|
|
||||||
val ws = Ws.connect(WSS_TEST_URL)
|
val ws = Ws.connect(WSS_TEST_URL)
|
||||||
@ -63,22 +64,149 @@ Secure websocket (`wss`) exchange:
|
|||||||
ws.close()
|
ws.close()
|
||||||
[ws.url() == WSS_TEST_URL, m.text]
|
[ws.url() == WSS_TEST_URL, m.text]
|
||||||
>>> [true,secure:ping]
|
>>> [true,secure:ping]
|
||||||
|
```
|
||||||
|
|
||||||
---
|
## Message Flow And Session Lifecycle
|
||||||
|
|
||||||
#### API reference
|
### Reading Incoming Messages
|
||||||
|
|
||||||
##### `Ws` (static methods)
|
Call `ws.receive()` to wait for the next application message.
|
||||||
|
|
||||||
- `isSupported(): Bool` — Whether WebSocket client support is available on the current runtime.
|
What `receive()` returns:
|
||||||
- `connect(url: String, headers...): WsSession` — Open a client websocket session.
|
- `WsMessage` for the next text or binary message.
|
||||||
|
- `null` after the peer closes the connection cleanly.
|
||||||
|
- `null` after 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()` returns `null`.
|
||||||
|
|
||||||
|
Typical receive loop:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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(...)` and `sendBytes(...)` 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() == null` tells you the receive side has reached the end of the websocket session.
|
||||||
|
|
||||||
|
Practical rule:
|
||||||
|
- If `receive()` returns `null`, 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.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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 = 1000` and `reason = ""`.
|
||||||
|
- 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()` returns `null` and further sends fail.
|
||||||
|
|
||||||
|
### Recommended Usage Pattern
|
||||||
|
|
||||||
|
For request-response style exchanges:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
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:
|
`headers...` accepts:
|
||||||
|
- `MapEntry`, for example `"Authorization" => "Bearer x"`
|
||||||
|
- 2-item lists, for example `["Authorization", "Bearer x"]`
|
||||||
|
|
||||||
- `MapEntry`, e.g. `"Authorization" => "Bearer x"`
|
### `WsSession`
|
||||||
- 2-item lists, e.g. `["Authorization", "Bearer x"]`
|
|
||||||
|
|
||||||
##### `WsSession`
|
|
||||||
|
|
||||||
- `isOpen(): Bool`
|
- `isOpen(): Bool`
|
||||||
- `url(): String`
|
- `url(): String`
|
||||||
@ -87,24 +215,27 @@ Secure websocket (`wss`) exchange:
|
|||||||
- `receive(): WsMessage?`
|
- `receive(): WsMessage?`
|
||||||
- `close(code: Int = 1000, reason: String = ""): void`
|
- `close(code: Int = 1000, reason: String = ""): void`
|
||||||
|
|
||||||
`receive()` returns `null` after a clean close.
|
Behavior summary:
|
||||||
|
- `receive()` returns `null` after close.
|
||||||
|
- `close()` is safe to call more than once.
|
||||||
|
- send operations require an open session.
|
||||||
|
|
||||||
##### `WsMessage`
|
### `WsMessage`
|
||||||
|
|
||||||
- `isText: Bool`
|
- `isText: Bool`
|
||||||
- `text: String?`
|
- `text: String?`
|
||||||
- `data: Buffer?`
|
- `data: Buffer?`
|
||||||
|
|
||||||
Text messages populate `text`; binary messages populate `data`.
|
Payload rules:
|
||||||
|
- Text messages populate `text` and leave `data == null`.
|
||||||
|
- Binary messages populate `data` and leave `text == null`.
|
||||||
|
|
||||||
---
|
## Security Policy
|
||||||
|
|
||||||
#### Security policy
|
|
||||||
|
|
||||||
The module uses `WsAccessPolicy` to authorize websocket operations.
|
The module uses `WsAccessPolicy` to authorize websocket operations.
|
||||||
|
|
||||||
- `WsAccessPolicy` — interface for custom policies
|
- `WsAccessPolicy` - interface for custom policies.
|
||||||
- `PermitAllWsAccessPolicy` — allows all websocket operations
|
- `PermitAllWsAccessPolicy` - allows all websocket operations.
|
||||||
- `WsAccessOp.Connect(url)`
|
- `WsAccessOp.Connect(url)`
|
||||||
- `WsAccessOp.Send(url, bytes, isText)`
|
- `WsAccessOp.Send(url, bytes, isText)`
|
||||||
- `WsAccessOp.Receive(url)`
|
- `WsAccessOp.Receive(url)`
|
||||||
@ -137,14 +268,12 @@ val allowLocalOnly = object : WsAccessPolicy {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Platform Support
|
||||||
|
|
||||||
#### Platform support
|
- **JVM:** supported.
|
||||||
|
- **Android:** supported via the Ktor CIO websocket client backend.
|
||||||
- **JVM:** supported
|
- **JS:** supported via the Ktor JS websocket client backend.
|
||||||
- **Android:** supported via the Ktor CIO websocket client backend
|
- **Linux native:** supported via the Ktor Curl websocket client backend.
|
||||||
- **JS:** supported via the Ktor JS websocket client backend
|
- **Windows native:** supported via the Ktor WinHttp websocket client backend.
|
||||||
- **Linux native:** supported via the Ktor Curl websocket client backend
|
- **Apple native:** supported via the Ktor Darwin websocket client backend.
|
||||||
- **Windows native:** supported via the Ktor WinHttp websocket client backend
|
- **Other targets:** may report unsupported; use `Ws.isSupported()` before relying on websocket client access.
|
||||||
- **Apple native:** supported via the Ktor Darwin websocket client backend
|
|
||||||
- **Other targets:** may report unsupported; use `Ws.isSupported()` before relying on websocket client access
|
|
||||||
|
|||||||
@ -3245,6 +3245,21 @@ class Compiler(
|
|||||||
operand = StatementRef(s)
|
operand = StatementRef(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"break" -> {
|
||||||
|
val s = parseBreakStatement(t.pos)
|
||||||
|
operand = StatementRef(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
"continue" -> {
|
||||||
|
val s = parseContinueStatement(t.pos)
|
||||||
|
operand = StatementRef(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
"return" -> {
|
||||||
|
val s = parseReturnStatement(t.pos)
|
||||||
|
operand = StatementRef(s)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Do not consume the keyword as part of a term; backtrack
|
// Do not consume the keyword as part of a term; backtrack
|
||||||
// and return null so outer parser handles it.
|
// and return null so outer parser handles it.
|
||||||
@ -4784,6 +4799,28 @@ class Compiler(
|
|||||||
return inferTypeDeclFromRef(directRef)
|
return inferTypeDeclFromRef(directRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isAbruptControlRef(ref: ObjRef): Boolean {
|
||||||
|
val stmt = (ref as? StatementRef)?.statement ?: return false
|
||||||
|
return when (unwrapBytecodeDeep(stmt)) {
|
||||||
|
is BreakStatement, is ContinueStatement, is ReturnStatement, is ThrowStatement -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferElvisTypeDecl(ref: ElvisRef): TypeDecl? {
|
||||||
|
val leftType = inferTypeDeclFromRef(ref.left)
|
||||||
|
?: inferObjClassFromRef(ref.left)?.let { TypeDecl.Simple(it.className, false) }
|
||||||
|
val nonNullLeftType = leftType?.let { makeTypeDeclNonNullable(it) }
|
||||||
|
if (isAbruptControlRef(ref.right)) return nonNullLeftType
|
||||||
|
val rightType = inferTypeDeclFromRef(ref.right)
|
||||||
|
?: inferObjClassFromRef(ref.right)?.let { TypeDecl.Simple(it.className, false) }
|
||||||
|
return when {
|
||||||
|
nonNullLeftType == null -> rightType
|
||||||
|
rightType == null -> nonNullLeftType
|
||||||
|
else -> mergeTypeDecls(nonNullLeftType, rightType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferTypeDeclFromRef(ref: ObjRef): TypeDecl? {
|
private fun inferTypeDeclFromRef(ref: ObjRef): TypeDecl? {
|
||||||
resolveReceiverTypeDecl(ref)?.let { return it }
|
resolveReceiverTypeDecl(ref)?.let { return it }
|
||||||
return when (ref) {
|
return when (ref) {
|
||||||
@ -4792,6 +4829,7 @@ class Compiler(
|
|||||||
is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
|
is MapLiteralRef -> inferMapLiteralTypeDecl(ref)
|
||||||
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
|
is ConstRef -> inferTypeDeclFromConst(ref.constValue)
|
||||||
is RangeRef -> TypeDecl.Simple("Range", false)
|
is RangeRef -> TypeDecl.Simple("Range", false)
|
||||||
|
is ElvisRef -> inferElvisTypeDecl(ref)
|
||||||
is CallRef -> {
|
is CallRef -> {
|
||||||
val targetDecl = resolveReceiverTypeDecl(ref.target) ?: seedTypeDeclFromRef(ref.target)
|
val targetDecl = resolveReceiverTypeDecl(ref.target) ?: seedTypeDeclFromRef(ref.target)
|
||||||
val targetName = when (val target = ref.target) {
|
val targetName = when (val target = ref.target) {
|
||||||
@ -5306,6 +5344,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref)
|
is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref)
|
||||||
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
||||||
|
is ElvisRef -> inferElvisTypeDecl(ref)
|
||||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,4 +134,23 @@ class OptTest {
|
|||||||
assertEquals((1..10).toSet(), result)
|
assertEquals((1..10).toSet(), result)
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testElvisBreak() = runTest {
|
||||||
|
eval("""
|
||||||
|
fun t(x: Int?): Int? =
|
||||||
|
if( x == null || x == 3 ) null
|
||||||
|
else 100
|
||||||
|
fun needInt(x: Int): Int = x
|
||||||
|
|
||||||
|
var cnt = -1
|
||||||
|
while( true ) {
|
||||||
|
val x = t(cnt++) ?: break
|
||||||
|
assertEquals(100, x)
|
||||||
|
assertEquals(100, needInt(x))
|
||||||
|
}
|
||||||
|
assert( t(3) == null )
|
||||||
|
assert( cnt == 4 )
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user