Serve a WebSocket chat room
Upgrade a route to a WebSocket and pass each member's messages to the rest of the room.
Generated by cargo run -p agent_docs from compiler/examples/. Edit the example files, then regenerate.
Upgrade a route to a WebSocket and pass each member's messages to the rest of the room.
The route is an ordinary Serve handler. WebSocket.handshake(req) checks the opening
handshake without touching the connection, so a request that is not one is answered 426 with
sec-websocket-version: 13. WebSocket.accept(req, options, member) then upgrades it and runs
member with the Socket: the same send, receive and close a client has, closed with
1001 when member returns, fails or is cancelled. It returns the 101, and the server discards
what the handler returns for a connection it upgraded. When no upgrade happened it fails
SocketError(Accept(reason)): a browser page from an origin other than chat.example.com is
answered 403, and anything else, such as the SAKURA_MAX_SOCKETS limit, 503 with
retry-after. origins starts empty, which refuses every browser, since a browser sends the
victim's cookies with the upgrade; a client that sends no origin is admitted, and
authentication stays the handler's.
A Socket cannot be kept in handler state (V6003), so the room is a topic:
WebSocket.subscribe(sock, "room"), and WebSocket.publish("room", message, Some(sock))
writes to every other member without waiting, passing over one that is not reading. The runtime
has no idle timer and sends no pings: timeout around receive closes a member that has been
silent for five minutes. max_message caps each message at 64 KiB in both directions.
The manifest grants the listener's address; accept opens nothing, so it needs no entry of
its own. The tests run Serve.scripted around WebSocket.scripted, which plays the member, and
print Get /chat -> 101 for the upgrade. No test opens a socket.
Source project · harness mode: test.
From the repository root, with this checkout's sakura on PATH:
cd compiler/examples/websocket_server
sakura check --json
sakura test --jsonManifest and permissions
[package]
name = "websocket_server"
version = "0.1.0"
edition = "2026"
[permissions]
net = ["127.0.0.1:8080"]Source and tests
src/main.sak includes all required imports and tests.
import std/request.{Get, Request}
import std/response.{Response}
import std/router.{Router}
import std/serve.{Address, Serve, ServeError}
import std/task.{timeout}
import std/url.{Url}
import std/websocket.{
Accept,
Binary,
Close,
Closed,
Data,
Reply,
Socket,
SocketError,
Text,
Upgrade,
WebSocket,
}
/// The chat route. A request that is not a WebSocket handshake is answered 426 with the version
/// the server speaks; a browser page from an origin other than the chat's own is 403; a server
/// at its socket limit is 503. An accepted member joins the `room` topic, and each text message
/// it sends goes to every other member. A member silent for five minutes is closed with 4000.
pub fn chat(req: Request) -> Response ! {Clock, Task, WebSocket} {
match WebSocket.handshake(req) {
Err(why) =>
Response { ..Response.status(426, why), headers: [("sec-websocket-version", "13")] },
Ok(_) => join(req),
}
}
fn join(req: Request) -> Response ! {Clock, Task, WebSocket} {
let options = Upgrade {
..WebSocket.upgrade(),
origins: ["https://chat.example.com"],
max_message: 65_536,
}
match try { WebSocket.accept(req, options, member) } {
Ok(switched) => switched,
Err(e) => match e.reason {
Accept(why) if String.starts_with(why, "origin ") => Response.status(403, why),
_ => Response { ..Response.status(503, "busy"), headers: [("retry-after", "1")] },
},
}
}
fn member(sock: Socket) -> Unit ! {Clock, Task, WebSocket, Fail<SocketError>} {
WebSocket.subscribe(sock, "room")
WebSocket.send(sock, Text("welcome"))
var open = true
while open {
match try { timeout(Duration.seconds(300), fn() { WebSocket.receive(sock) }) } {
Ok(Data(Text(s))) => { let _ = WebSocket.publish("room", Text(s), Some(sock)) },
Ok(Data(Binary(_))) => {},
Ok(Closed(_)) => { open = false },
Err(_) => {
WebSocket.close(sock, 4000, "idle")
open = false
},
}
}
}
pub fn router() -> Router<{Clock, Task, WebSocket}> { Router.new().get("/chat", chat) }
test "a member is welcomed, talks, and leaves at the close; the upgrade answers 101" {
let script = [Reply(Text("welcome"), Text("hello room")), Close(1000, "bye")]
with Serve.scripted([WebSocket.upgrade_request("/chat", [])]),
WebSocket.scripted(script),
Clock.virtual(),
Task.deterministic(1) {
expect_fail(
fn() { Serve.listen(router(), Address.of("127.0.0.1", 8080)) },
ServeError { reason: "no more requests" },
)
}
expect_output(["Get /chat -> 101 "])
}
test "a plain GET is 426, a foreign browser origin 403" {
let foreign = Request.with_header(
WebSocket.upgrade_request("/chat", []),
"origin",
"https://evil.example",
)
let plain = Request.with_header(Request.of(Get, Url.from("/chat")), "host", "example.test")
with Serve.scripted([plain, foreign]),
WebSocket.scripted([]),
Clock.virtual(),
Task.deterministic(1) {
let _ = try { Serve.listen(router(), Address.of("127.0.0.1", 8080)) }
}
expect_output([
"Get /chat -> 426 upgrade does not name websocket",
"Get /chat -> 403 origin https://evil.example is not accepted",
])
}Compiler provenance and regeneration
Finish only when no holes remain, independent tests pass, the requested target builds, and contracts/tests/permissions have not been weakened. Repeat the frozen baseline check after the final edits. Report missing prerequisites and failing checks explicitly; do not claim a skipped test passed.
These projects teach supported Sakura APIs on the JavaScript backend. Each
project is exercised offline by cargo test -p sakura_cli --test examples;
HTTP and LLM exchanges are synthetic committed cassettes, with no network or API keys.
Use Node 26 for execution, including its built-in SQLite implementation.
The initial API baseline is compiler revision
5cae349516c21714d90d36e87f1cfd6debe980c6.
CI reruns these projects against the compiler in the same checkout. Build that
checkout's CLI and run sakura --version and sakura prompt before adapting a recipe.
Sakura Lang is pre-1.0: read STABILITY.md before depending on an API. An installed
skill's frontmatter records its compiler version and build revision. These teaching examples are separate from the agent
evaluation tasks.
The source of each page is its example's recipe.md, sakura.toml,
src/main.sak and fixture files. Run cargo run -p agent_docs from compiler/
to regenerate the cookbook and skill reference. Edit those sources, not the rendered pages.
Last updated