# Subscribe to a WebSocket feed

URL: /docs/cookbook/websocket_client
Markdown: /docs/cookbook/websocket_client.md

> Open a WebSocket, subscribe to a topic and collect its updates until the server closes.

Generated by `cargo run -p agent_docs` from `compiler/examples/`. Edit the example files, then regenerate.

Open a WebSocket, subscribe to a topic and collect its updates until the server closes.

`WebSocket.connect` opens the connection and runs the callback with the `Socket`; when the
callback returns, fails or is cancelled the connection is closed (1001 if it is still open), and
returning the socket from the callback is `V6003`. There are no event listeners: a message or
the close is a value `WebSocket.receive` returns, and `WebSocket.messages(sock)` is the same as a
source that ends at the close, so `filter`, `map` and `to_list(max)` read it with a stated
bound. `max_message` caps each message at 64 KiB in both directions; a larger one from the
server ends the connection with 1009.

The manifest grants `feed.example.com:443`, the port a `wss` URL uses by default; a `main` that
declares `WebSocket` with an empty `net` list exits 75 at startup. The tests run against
`WebSocket.scripted`: `Reply` answers the subscription, `Push` sends updates, `Close` ends the
feed, and a send the script does not expect fails with `SocketError(Failed(...))` instead of
waiting. No test opens a socket.

[Source project](https://github.com/icantcodefyi/sakuralang/tree/main/compiler/examples/websocket_client) · harness mode: `test`.

From the repository root, with this checkout's `sakura` on `PATH`:

```sh
cd compiler/examples/websocket_client
sakura check --json
sakura test --json
```

### Manifest and permissions [#manifest-and-permissions]

```toml
[package]
name = "websocket_client"
version = "0.1.0"
edition = "2026"

[permissions]
net = ["feed.example.com:443"]
```

### Source and tests [#source-and-tests]

`src/main.sak` includes all required imports and tests.

```sakura test id=cookbook-websocket_client project=websocket_client
import std/stream.{Source, StreamError}
import std/websocket.{Binary, Close, Message, Options, Push, Reply, SocketError, Text, WebSocket}

/// Subscribes to `topic` on a feed server and returns up to `max` text updates, in order, until
/// the server closes the connection. A binary frame is skipped. Every message is at most 64 KiB.
pub fn updates(url: String, topic: String, max: Int) -> List<String>
  ! {WebSocket, Fail<SocketError | StreamError>}
  requires max >= 0
{
  let opts = Options { ..WebSocket.options(), subprotocols: ["feed.v1"], max_message: 65_536 }
  WebSocket.connect(url, opts, fn(sock) {
    WebSocket.send(sock, Text("subscribe {topic}"))
    WebSocket.messages(sock).filter(is_text).map(text_of).to_list(max)
  })
}

fn is_text(m: Message) -> Bool {
  match m {
    Text(_) => true,
    Binary(_) => false,
  }
}

fn text_of(m: Message) -> String {
  match m {
    Text(s) => s,
    Binary(_) => "",
  }
}

test "updates arrive in order until the server closes" {
  let script = [
    Reply(Text("subscribe prices"), Text("ok")),
    Push(Text("BTC 1")),
    Push(Binary(Bytes.from_list([0]))),
    Push(Text("BTC 2")),
    Close(1000, "end of feed"),
  ]
  with WebSocket.scripted(script) {
    expect_eq(
      try { updates("wss://feed.example.com/live", "prices", 10) },
      Ok(["ok", "BTC 1", "BTC 2"]),
    )
  }
}

test "more updates than asked for fail instead of growing" {
  let script = [Reply(Text("subscribe a"), Text("1")), Push(Text("2")), Push(Text("3"))]
  with WebSocket.scripted(script) {
    let got = try { updates("wss://feed.example.com/live", "a", 2) }
    expect(String.contains("{got}", "MaxItems"))
  }
}

test "a server that answers the wrong subscription is a typed failure" {
  with WebSocket.scripted([Reply(Text("subscribe b"), Text("ok"))]) {
    let got = try { updates("wss://feed.example.com/live", "a", 10) }
    expect(String.contains("{got}", "Failed"))
  }
}
```

### Compiler provenance and regeneration [#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 ten 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](https://github.com/icantcodefyi/sakuralang/commit/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.
