15 WebSocket
Everything we’ve covered so far follows one pattern: the client sends a request, the server sends back a response, and the connection closes. The client always initiates.
But what if the server needs to push data to the client without being asked? A live chat message. A notification. A real-time dashboard update.
That’s where the WebSocket protocol comes in. Unlike HTTP, a WebSocket connection stays open. Both sides can send messages at any time. Communication is bi-directional.
Ambiorix has built-in WebSocket support.
15.1 The Message Protocol
Before we look at any code, you need to understand how Ambiorix structures WebSocket messages.
Every message – whether sent from the server or the client – must be a JSON object with three fields:
{
"name": "greeting",
"message": "Hello!",
"isAmbiorix": true
}name: A string identifier. This is how Ambiorix routes messages to the right handler, much like a URL path routes HTTP requests.message: The payload. Any JSON-serializable value.isAmbiorix: Must beTRUE. Messages without this flag are silently ignored.
This structure applies in both directions. The R server and the JavaScript client both speak the same format.
15.2 Receiving Messages
Use app$receive() to listen for incoming WebSocket messages. It takes:
name: The message name to listen for.handler: A function to run when that message arrives.
The handler function accepts up to two arguments:
msg: The message content (themessagefield from the JSON).ws: The WebSocket connection object, which you can use to send a response back.
app$receive("greeting", function(msg, ws) {
cat("Received:", msg, "\n")
ws$send("greeting", "Hello from the server!")
})If you don’t need to send a response, you can drop the ws argument:
app$receive("log", function(msg) {
cat("Client says:", msg, "\n")
})Ambiorix checks the number of arguments in your handler and adapts accordingly.
15.3 Sending Messages
The ws$send() method takes two arguments:
name: The message identifier.message: The content to send. Anything that can be serialized to JSON.
ws$send(name = "update", message = list(count = 42L))Ambiorix wraps this in the required {name, message, isAmbiorix} structure and serializes it to JSON automatically.
15.4 The JavaScript Client
On the client side, Ambiorix provides a small JavaScript library – ambiorix.js – that handles the WebSocket connection and message formatting for you.
If you set up your project with {ambiorix.generator}, this file is placed in your static directory automatically. Otherwise, you can copy it manually:
ambiorix::copy_websocket_client(path = "static/ambiorix.js")Include it in your HTML:
<script src="/static/ambiorix.js"></script>15.4.1 Sending from the Client
Ambiorix.send() is a static method. You don’t need to instantiate anything:
Ambiorix.send("greeting", "Hello from the browser!");This sends a properly formatted JSON message to the server.
15.4.2 Receiving on the Client
To handle messages from the server, instantiate the class, register your handlers with receive(), and call start():
var wss = new Ambiorix();
wss.receive("greeting", function(msg) {
alert(msg);
});
wss.start();start() is what actually attaches the event listeners. Without it, your handlers won’t fire.
15.6 Broadcasting
Sending a message via ws$send() replies only to the client that sent the original message. In many real-time applications – chat, live feeds, collaborative editing – you need to send a message to all connected clients.
Ambiorix does not have a built-in broadcast method. Instead, it tracks every connected WebSocket client in a list. Use get_websocket_clients() to access it:
app$receive("chat", function(msg, ws) {
clients <- get_websocket_clients()
lapply(clients, function(client) {
client$send("chat", msg)
})
})When any client sends a "chat" message, the handler iterates over all connected clients and forwards the message to each one.
15.6.1 Example: Chat
Here’s a minimal chat application. Every message sent by any client is broadcast to all connected clients.
The HTML (templates/chat.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/static/ambiorix.js"></script>
<script>
var wss = new Ambiorix();
wss.receive("chat", function(msg) {
var li = document.createElement("li");
li.textContent = msg;
document.getElementById("messages").appendChild(li);
});
wss.start();
function sendMessage() {
var input = document.getElementById("msg");
Ambiorix.send("chat", input.value);
input.value = "";
}
</script>
<title>Chat</title>
</head>
<body>
<h1>Chat</h1>
<ul id="messages"></ul>
<input type="text" id="msg" placeholder="Type a message...">
<button onclick="sendMessage()">Send</button>
</body>
</html>The server:
library(ambiorix)
app <- Ambiorix$new()
app$static("static", "static")
app$get("/", function(req, res) {
res$send_file("chat.html")
})
app$receive("chat", function(msg, ws) {
clients <- get_websocket_clients()
lapply(clients, function(client) {
client$send("chat", msg)
})
})
app$start(port = 3000L)Open the app in two browser tabs. Type a message in one and it appears in both.
15.7 R as a WebSocket Client
So far the client has been a browser running JavaScript. But you can also connect to an Ambiorix WebSocket server from another R session using the {websocket} package.
The catch: you have to construct the message JSON yourself. The isAmbiorix field is required, and the message must be serialized to a JSON string.
client <- websocket::WebSocket$new(
"ws://127.0.0.1:3000",
autoConnect = FALSE
)
client$onOpen(function(event) {
cat("Connection opened\n")
msg <- list(
isAmbiorix = TRUE,
name = "greeting",
message = "Hello from R client!"
)
client$send(yyjsonr::write_json_str(msg, auto_unbox = TRUE))
})
client$onMessage(function(event) {
cat("Server says:", event$data, "\n")
})
client$connect()Notes:
autoConnect = FALSElets you set up handlers before connecting.isAmbiorix = TRUEis mandatory. Without it, Ambiorix ignores the message.{yyjsonr}is used for serialization because that’s what Ambiorix uses internally. You could also use{jsonlite}.
15.8 Bypassing the Protocol
Everything above uses Ambiorix’s convenience layer: named messages, automatic JSON serialization, and the receive() routing mechanism.
If you need full control over the raw WebSocket connection – for example, to handle binary data or use a different message format – you can override the default handler entirely:
app$websocket <- function(ws) {
ws$onMessage(function(binary, message) {
cat("Raw message:", message, "\n")
})
}This replaces Ambiorix’s internal WebSocket handling. The receive() handlers you registered will no longer fire. You’re working directly with the {httpuv} WebSocket object.
Use this only when the built-in protocol doesn’t fit your use case.