Sakura Lang

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 --json

Manifest 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.

Edit on GitHub

Last updated

On this page