# ws

> Run WebSocket servers and connect to them as a client, sending text, binary and JSON messages in both directions.

This is the Markdown version of https://osl-lang.org/packages/ws. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:ws"
```

`std:ws` speaks WebSocket: a connection that stays open so either side can send a message at any time. Reach for it for chat, live dashboards, notifications and games, anywhere polling over plain HTTP would be wasteful. A server is usually mounted on a [serve](/packages/serve.md) router, and a client connects with `ws.connect`.

Every example below runs a server and a client in the one program on `127.0.0.1`. Messages arrive on another thread, so the examples collect them in a channel from [thread](/packages/thread.md) and read them in order.

## An echo server and a client

`ws.new()` makes a server. `server.on("message", fn)` registers a handler that receives the connection and the text. Mount the server on a route with `app.ws(path, server)`, then connect with `ws.connect`:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

auto app = serve.new()
auto echo = ws.new()

echo.on("message", def(*ws.Connection conn, string message) (
  conn.send("echo: " ++ message).unwrap()
))
app.ws("/echo", echo)
app.start("127.0.0.1:8780").unwrap()

auto client = ws.connect("ws://127.0.0.1:8780/echo").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))

client.send("hello").unwrap()
log inbox.recv()
client.send("again").unwrap()
log inbox.recv()

client.close({wait: true})
app.stop().unwrap()
```

Output:

```text
echo: hello
echo: again
```

Send a third message, or an empty string, and run it again.

`send` returns a result because it can fail: the connection may be closed, or the peer too slow to read. `unwrap()` is fine in an example, and real code checks it. Handlers run on the connection's own thread, which is why the client hands what it receives to a channel. `inbox.recv()` waits until a message is there.

## Events

A server has three events: `connect` and `disconnect` call `fn(conn)`, and `message` calls `fn(conn, message)`. A client connection has `message`, `close`, and `open` (after a reconnect). Add as many handlers as you like. `off` removes one.

```osl
import "std:ws"
import "std:serve"
import "std:thread"

auto app = serve.new()
auto server = ws.new()
auto events = thread.channel(8)

server.on("connect", def(*ws.Connection conn) (
  events.send("connected").unwrap()
  conn.send("welcome").unwrap()
))
server.on("disconnect", def(*ws.Connection conn) (
  events.send("disconnected").unwrap()
))
app.ws("/", server)
app.start("127.0.0.1:8781").unwrap()

auto client = ws.connect("ws://127.0.0.1:8781/").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))
log events.recv()
log inbox.recv()
log client.isConnected()

client.close({wait: true})
log events.recv()
log client.isConnected()
app.stop().unwrap()
```

Output:

```text
connected
welcome
true
disconnected
false
```

The server calls the `connect` handlers before it delivers the connection's first message, so a handler there can set things up without racing the client.

## Messages: text, bytes and JSON

`send` picks the format from the value. A string goes as a text message, `byte[]` as a binary message, and an object or array is encoded as JSON text. On the receiving side every message arrives as a string, so parse JSON with [json](/packages/json.md):

```osl
import "std:ws"
import "std:serve"
import "std:json"
import "std:thread"

auto app = serve.new()
auto server = ws.new()

server.on("message", def(*ws.Connection conn, string message) (
  auto request = json.parse<object>(message).unwrap()
  conn.send({sum: request["a"] + request["b"], received: message.len}).unwrap()
))
app.ws("/add", server)
app.start("127.0.0.1:8782").unwrap()

auto client = ws.connect("ws://127.0.0.1:8782/add").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))

client.send({a: 2, b: 40}).unwrap()
log inbox.recv()

client.close({wait: true})
app.stop().unwrap()
```

Output:

```text
{"received":14,"sum":42}
```

Messages over 1 MiB are refused by default. Raise the limit with the `maxMessageBytes` option of `ws.new` and `ws.connect` when you need larger ones.

## Broadcast and rooms

`server.broadcast(message)` sends to every connected client. To send to a subset, tag connections with `conn.set(key, value)` and call `server.broadcastWhere(key, value, message)`. Here two clients join different rooms from a header, and a message reaches only the sender's room:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

auto app = serve.new()
auto chat = ws.new()

chat.on("connect", def(*ws.Connection conn) (
  conn.set("room", conn.header("X-Room") ?? "lobby")
))
chat.on("message", def(*ws.Connection conn, string message) (
  string room = conn.get("room")
  chat.broadcastWhere("room", room, room ++ ": " ++ message)
))
app.ws("/chat", chat)
app.start("127.0.0.1:8783").unwrap()

auto red = ws.connect("ws://127.0.0.1:8783/chat", {headers: {"X-Room": "red"}}).unwrap()
auto blue = ws.connect("ws://127.0.0.1:8783/chat", {headers: {"X-Room": "blue"}}).unwrap()
auto redInbox = thread.channel(4)
auto blueInbox = thread.channel(4)
red.on("message", def(*ws.Connection conn, string message) (
  redInbox.send(message).unwrap()
))
blue.on("message", def(*ws.Connection conn, string message) (
  blueInbox.send(message).unwrap()
))

red.send("hello red").unwrap()
blue.send("hello blue").unwrap()
log redInbox.recv()
log blueInbox.recv()
log chat.connections().len

red.close({wait: true})
blue.close({wait: true})
app.stop().unwrap()
```

Output:

```text
red: hello red
blue: hello blue
2
```

`conn.header(name)` reads a header of the upgrade request, so the same trick carries a token or a user name. Browsers can't set custom headers on a WebSocket, so for those clients put the value in the URL and read it from the `serve` context in middleware instead.

## Serving on a path of your own

Without `serve`, a server can listen by itself. Give `ws.new` an address and path and call `run()`, which blocks until `stop()`, so run it in an async function:

```osl
import "std:ws"
import "std:thread"

auto server = ws.new({address: "127.0.0.1:8784", path: "/live"})
server.on("message", def(*ws.Connection conn, string message) (
  conn.send(message.toUpper()).unwrap()
))

async listen() result<unit, error> (
  return server.run()
)
auto running = listen()
thread.sleep(300)

auto client = ws.connect("ws://127.0.0.1:8784/live").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))
client.send("quiet please").unwrap()
log inbox.recv()

client.close({wait: true})
server.stop().unwrap()
log running.wait().isOk()
```

Output:

```text
QUIET PLEASE
true
```

Prefer mounting on `serve` when the program has other routes: one port, shared middleware and one place to stop.

## Closing

`conn.close()` returns at once and shuts the connection down in the background: queued messages go first, then a close message. Options set a `code` and `reason` the other side sees, and `wait: true` returns only when the socket is closed. After closing, `send` fails with a `StateError`:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

auto app = serve.new()
auto server = ws.new()
server.on("message", def(*ws.Connection conn, string message) (
  conn.send("ok: " ++ message).unwrap()
  if message == "bye" (
    conn.close({code: 1000, reason: "finished"})
  )
))
app.ws("/", server)
app.start("127.0.0.1:8785").unwrap()

auto client = ws.connect("ws://127.0.0.1:8785/").unwrap()
auto inbox = thread.channel(8)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))
client.on("close", def(*ws.Connection conn) (
  inbox.send("closed").unwrap()
))

client.send("bye").unwrap()
log inbox.recv()
log inbox.recv()
log client.send("too late").unwrapErr().kind

app.stop().unwrap()
```

Output:

```text
ok: bye
closed
StateError
```

## Failing to connect

`ws.connect` returns a result. The error kind says what went wrong: `NetworkError` for a refused connection, `TimeoutError` when the handshake takes longer than the `timeout` option (10 seconds by default), `UnsupportedError` for a URL that isn't `ws://` or `wss://`, `ParseError` for a malformed URL and `NotFoundError` when the host doesn't exist:

```osl
import "std:ws"

auto refused = ws.connect("ws://127.0.0.1:1/nothing")
log refused.isErr() refused.unwrapErr().kind

auto wrong = ws.connect("http://example.com")
log wrong.unwrapErr().kind

switch ws.connect("ws://127.0.0.1:1/") (
  case ok(conn)
    log "connected"
  case err(e)
    log "could not connect:" e.kind
)
```

Output:

```text
true NetworkError
UnsupportedError
could not connect: NetworkError
```

## Reconnecting

A client connection can reopen itself after it drops. `conn.enableReconnect()` retries with a delay that doubles from one second to thirty, and fires `open` each time it succeeds. `conn.close()` stops it for good:

```osl
import "std:ws"

auto client = ws.connect("wss://example.com/feed").unwrap()
client.enableReconnect()
client.on("open", def(*ws.Connection conn) (
  log "reconnected, resubscribing"
  conn.send({subscribe: "prices"}).unwrap()
))
client.on("message", def(*ws.Connection conn, string message) (
  log message
))
```

After a reconnect the server sees a brand new connection, so anything it knew about the old one (rooms, a user name) has to be sent again from the `open` handler.

## Securing a server

A server accepts upgrades only from pages on its own origin unless you call `server.allowAllOrigins()`. Do that only for a public API. Because the upgrade is an ordinary HTTP request, middleware on the route applies to it, so `serve.requireBearer` or your own check can refuse a socket before it opens. For `wss://`, use `server.runTLS(certFile, keyFile)` or `app.runTLS(...)`, or put a proxy that terminates TLS in front.

**Quiz.** A client handler runs on the connection's own thread. How does the example above pass the messages to the main program?

- [ ] It prints them from inside the handler.
- [x] It sends them into a channel, and the main thread calls `recv()`.
- [ ] `ws.connect` returns them in an array.

Answer: A channel hands values between threads safely and lets `recv()` wait until one arrives.

### Exercise: Shout service

Make the server answer each message with the message upper-cased and its length, as JSON: `{"shout": "HI", "length": 2}`. The client part is written for you.

Starting code:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

def reply(*thread.Channel inbox) string (
  for _ in 1 to 50 (
    auto m = inbox.tryRecv()
    if m != null return m
    thread.sleep(20)
  )
  return "(no reply)"
)

auto app = serve.new()
auto server = ws.new()

// handle messages here

app.ws("/shout", server)
app.start("127.0.0.1:8786").unwrap()

auto client = ws.connect("ws://127.0.0.1:8786/shout").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))
for word in ["hi", "osl"] (
  client.send(word).unwrap()
  log reply(inbox)
)

client.close({wait: true})
app.stop().unwrap()
```

It should print:

```text
{"length":2,"shout":"HI"}
{"length":3,"shout":"OSL"}
```

Hint 1: Register `server.on("message", def(*ws.Connection conn, string message) ( ... ))` and call `conn.send` with an object.

Hint 2: `message.toUpper()` upper-cases text and `message.len` is its length.

A solution:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

def reply(*thread.Channel inbox) string (
  for _ in 1 to 50 (
    auto m = inbox.tryRecv()
    if m != null return m
    thread.sleep(20)
  )
  return "(no reply)"
)

auto app = serve.new()
auto server = ws.new()

server.on("message", def(*ws.Connection conn, string message) (
  conn.send({shout: message.toUpper(), length: message.len}).unwrap()
))

app.ws("/shout", server)
app.start("127.0.0.1:8854").unwrap()

auto client = ws.connect("ws://127.0.0.1:8854/shout").unwrap()
auto inbox = thread.channel(4)
client.on("message", def(*ws.Connection conn, string message) (
  inbox.send(message).unwrap()
))
for word in ["hi", "osl"] (
  client.send(word).unwrap()
  log reply(inbox)
)

client.close({wait: true})
app.stop().unwrap()
```

### Exercise: Visitor numbers

Tell each client who it is. When a client connects, the server sends `you are visitor N`, counting from 1. Two clients connect, and each prints its greeting.

Starting code:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

def firstMessage(*ws.Connection client) string (
  auto inbox = thread.channel(4)
  client.on("message", def(*ws.Connection conn, string message) (
    inbox.send(message).unwrap()
  ))
  for _ in 1 to 50 (
    auto m = inbox.tryRecv()
    if m != null return m
    thread.sleep(20)
  )
  return "(nothing)"
)

auto app = serve.new()
auto server = ws.new()

// count visitors on connect

app.ws("/", server)
app.start("127.0.0.1:8787").unwrap()

auto a = ws.connect("ws://127.0.0.1:8787/").unwrap()
log firstMessage(a)
auto b = ws.connect("ws://127.0.0.1:8787/").unwrap()
log firstMessage(b)

a.close({wait: true})
b.close({wait: true})
app.stop().unwrap()
```

It should print:

```text
you are visitor 1
you are visitor 2
```

Hint 1: Keep `int visitors = 0` outside the handler and add one in `server.on("connect", ...)`.

Hint 2: `conn.send("you are visitor " ++ visitors).unwrap()`.

A solution:

```osl
import "std:ws"
import "std:serve"
import "std:thread"

def firstMessage(*ws.Connection client) string (
  auto inbox = thread.channel(4)
  client.on("message", def(*ws.Connection conn, string message) (
    inbox.send(message).unwrap()
  ))
  for _ in 1 to 50 (
    auto m = inbox.tryRecv()
    if m != null return m
    thread.sleep(20)
  )
  return "(nothing)"
)

auto app = serve.new()
auto server = ws.new()
int visitors = 0

server.on("connect", def(*ws.Connection conn) (
  visitors = visitors + 1
  conn.send("you are visitor " ++ visitors).unwrap()
))

app.ws("/", server)
app.start("127.0.0.1:8855").unwrap()

auto a = ws.connect("ws://127.0.0.1:8855/").unwrap()
log firstMessage(a)
auto b = ws.connect("ws://127.0.0.1:8855/").unwrap()
log firstMessage(b)

a.close({wait: true})
b.close({wait: true})
app.stop().unwrap()
```

## API reference

## API reference

Every member of `std:ws`, generated from the source of OSL.

### Functions

#### `ws.closeConn(any conn) boolean`

**Deprecated.** Deprecated: use conn.close() instead.

closeConn closes conn and reports whether it was a connection.

#### `ws.connect(string url, any ...opts) result<*ws.Connection, error>`

connect opens a WebSocket connection and starts receiving messages;
register handlers with conn.on. Options: protocols, headers, timeout
(milliseconds, default 10000), maxMessageBytes (default 1 MiB), sendQueue
(default 256) and compression. Fails with ParseError, UnsupportedError,
NotFoundError, TimeoutError or NetworkError. Deprecated positional
protocols from argument 2 are still accepted.

Deprecated positional arguments from 2: pass {protocols: [...]} instead.

#### `ws.new(any ...opts) *ws.Server`

new creates a WebSocket server for app.ws(path, server) or standalone
server.run(). Options: address (default ":8080"), path (default "/"),
maxMessageBytes (default 1 MiB), sendQueue (default 256) and compression.
Deprecated positional address/path arguments are still accepted.

Deprecated positional arguments from 1: pass {address, path} instead.

#### `ws.newServer(string addr, string path) *ws.Server`

**Deprecated.** Deprecated: use ws.new({address: addr, path: path}) instead.

newServer creates a standalone server on addr and path.

#### `ws.send(any conn, any message) boolean`

**Deprecated.** Deprecated: use conn.send(message) instead, which reports why sending failed.

send queues a message on conn and reports whether it was queued; false
for anything that isn't a connection.

### `*ws.Connection`

#### `connection.close(object? options)`

close closes the connection for good and cancels reconnecting, and
returns at once: from then on send fails and no more messages arrive.
In the background, messages already passed to send go first, then a
close message with code (default 1000, a normal closure) and reason
(default ""), and the socket stays open until the peer answers with its
own close message, so the peer sees a clean close and every message sent
before it. Options: code (1000-1003, 1007-1014 or 3000-4999), reason (at
most 123 bytes), timeout, the most milliseconds to wait for the peer
before closing the socket anyway (default 2000; 0 closes it as soon as
the close message is out), and wait, true to return only once the socket
is closed. The queued messages and the close message get the same 10 s
write limit as any send. "close" fires after. Closing again, even while
the first close is still running, is a no-op that returns at once, or
with wait, once the socket is closed. Throws ValueError for an invalid code or reason.

#### `connection.connected() boolean`

**Deprecated.** Deprecated: use conn.isConnected() instead.

connected reports whether the connection is open.

#### `connection.data() object`

data returns every stored value as a new object.

#### `connection.delete(string key)`

delete removes the value stored under key.

#### `connection.enableReconnect()`

enableReconnect makes a client connection reopen itself after it drops,
retrying with a delay that doubles from 1 s up to 30 s, and firing "open"
each time it reconnects. close() stops it.

#### `connection.get(string key) any`

get returns the value stored under key, or null.

#### `connection.getAll() object`

**Deprecated.** Deprecated: use conn.data() instead.

getAll returns every stored value.

#### `connection.getHeader(string key) string`

**Deprecated.** Deprecated: use conn.header(name) instead, which returns null for a missing header.

getHeader returns an upgrade request header, or "" when it wasn't sent.

#### `connection.getHeaders() object`

**Deprecated.** Deprecated: use conn.headers() instead.

getHeaders returns the upgrade request's headers.

#### `connection.header(string name) string?`

header returns a header of the upgrade request that opened a server-side
connection, matched case-insensitively, or null when it wasn't sent. The
"Remote-Addr" entry holds the client's address. Client connections have
no request headers.

#### `connection.headers() string[string]`

headers returns the upgrade request's headers as an object, with the
first value of each; empty for client connections.

#### `connection.isConnected() boolean`

isConnected reports whether the connection is currently open.

#### `connection.off(string event, any fn) boolean`

off removes a handler added with on and reports whether it was
registered.

#### `connection.on(string event, any fn)`

on adds a handler for a connection event: "message" calls
fn(conn, message) for every message received (binary messages arrive as
strings), "close" calls fn(conn) when the connection closes from either
side, and "open" calls fn(conn) after a reconnect. Handlers run on the
connection's own thread; one that throws has its error printed and the
connection keeps running. Throws ValueError for an unknown event.

#### `connection.onClose(any handler)`

**Deprecated.** Deprecated: use conn.on("close", fn) instead, which adds a handler.

onClose replaces the connection's close handler.

#### `connection.onMessage(any handler)`

**Deprecated.** Deprecated: use conn.on("message", fn) instead, which adds a handler.

onMessage replaces the connection's message handler.

#### `connection.send(any message) result<unit, error>`

send queues message for delivery: a string is sent as a text message,
byte[] as a binary message, and an object or array as JSON text. Fails
with StateError when the connection is closed and IOError when its send
queue is full because the peer isn't reading. Throws TypeError for other
message types.

#### `connection.set(string key, any value)`

set stores a value on the connection under key, for the program's own
bookkeeping (a user name, a room). broadcastWhere matches on it.

#### `connection.shutdown()`

**Deprecated.** Deprecated: use conn.close() instead, which now also cancels reconnecting.

shutdown closes the connection and cancels reconnecting.

### `*ws.Server`

#### `server.allowAllOrigins()`

allowAllOrigins accepts upgrades from pages on any origin. By default
only same-origin browser pages may connect.

#### `server.broadcast(any message)`

broadcast sends message to every connected client, skipping any whose
connection has closed or whose send queue is full. message is a string
(text), byte[] (binary) or an object or array (JSON text). Throws
TypeError for other message types.

#### `server.broadcastWhere(string key, any value, any message)`

broadcastWhere sends message to every client whose stored value for key
(see conn.set) equals value, skipping closed connections. message takes
the same types as broadcast.

#### `server.connections() *ws.Connection[]`

connections returns the currently connected clients.

#### `server.getConnections() *ws.Connection[]`

**Deprecated.** Deprecated: use server.connections() instead.

getConnections returns the connected clients.

#### `server.off(string event, any fn) boolean`

off removes a handler added with on and reports whether it was
registered.

#### `server.on(string event, any fn)`

on adds a handler for a server event: "connect" and "disconnect" call
fn(conn), and "message" calls fn(conn, message) for every message from
any client. Handlers run on the connection's own thread; one that throws
has its error printed and the server keeps running. Throws ValueError for
an unknown event.

#### `server.onConnect(any handler)`

**Deprecated.** Deprecated: use server.on("connect", fn) instead, which adds a handler.

onConnect replaces the server's connect handler.

#### `server.onDisconnect(any handler)`

**Deprecated.** Deprecated: use server.on("disconnect", fn) instead, which adds a handler.

onDisconnect replaces the server's disconnect handler.

#### `server.onMessage(any handler)`

**Deprecated.** Deprecated: use server.on("message", fn) instead, which adds a handler.

onMessage replaces the server's message handler.

#### `server.run() result<unit, error>`

run serves WebSocket upgrades on the server's address and path and
blocks until stop() is called; run it in a thread to keep going. Fails
with NetworkError when the address can't be bound. Throws StateError when
the server is already running.

#### `server.runTLS(string certFile, string keyFile) result<unit, error>`

runTLS is run over TLS (wss://) with the certificate and key in the given
PEM files. Fails with NotFoundError or IOError when they can't be read.

#### `server.start() result<unit, error>`

**Deprecated.** Deprecated: use server.run() instead.

start serves until stopped.

#### `server.startTLS(string certFile, string keyFile) result<unit, error>`

**Deprecated.** Deprecated: use server.runTLS(certFile, keyFile) instead.

startTLS serves over TLS until stopped.

#### `server.stop() result<unit, error>`

stop closes the server and every connection, firing "disconnect" for
each. A run() call then returns. Stopping a server that isn't running
only closes its connections.
