Skip to content

Guide WebSockets

Kris Simon edited this page Feb 7, 2026 · 1 revision

WebSockets

ARO provides built-in WebSocket support for real-time bidirectional communication. WebSocket connections are established via HTTP Upgrade on the same port as the HTTP server.

Overview

WebSocket enables persistent, full-duplex communication channels. Unlike HTTP request-response cycles, WebSocket allows servers to push data to clients without polling.

Use cases:

  • Real-time updates (dashboards, notifications)
  • Chat applications
  • Live feeds (logs, sensor data, market data)
  • Collaborative features (multi-user editing)

Architecture

WebSocket shares the HTTP port via HTTP Upgrade:

HTTP Client                           ARO Application
    |                                        |
    |  GET /ws HTTP/1.1                     |
    |  Upgrade: websocket                   |
    |----------------------------------->   |
    |                                        |
    |  HTTP/1.1 101 Switching Protocols     |
    |<-----------------------------------   |
    |                                        |
    |  <-- WebSocket frames (bidirectional) |
    |                                        |

WebSocket Events

Three events manage the WebSocket lifecycle:

Event Triggered When
Connect Client completes WebSocket handshake
Message Client sends a text message
Disconnect Connection closes

Event Handlers

Use the WebSocket Event Handler business activity pattern:

(Handler Name: WebSocket Event Handler)

The handler name determines which event triggers it:

  • Contains "Connect" → connection events
  • Contains "Message" → message events
  • Contains "Disconnect" → disconnection events

Connection Handler

(Handle WebSocket Connect: WebSocket Event Handler) {
    <Extract> the <connection-id> from the <event: id>.
    <Extract> the <path> from the <event: path>.
    <Log> "WebSocket client connected" to the <console>.
    <Return> an <OK: status> for the <connection>.
}

Event properties:

Property Type Description
id String Unique connection identifier
path String WebSocket path (e.g., "/ws")
remoteAddress String Client IP address

Message Handler

(Handle WebSocket Message: WebSocket Event Handler) {
    <Extract> the <message> from the <event: message>.
    <Extract> the <connection-id> from the <event: connectionId>.
    <Log> "Received message" to the <console>.
    <Return> an <OK: status> for the <message>.
}

Event properties:

Property Type Description
message String Text message content
connectionId String Sender's connection ID

Disconnection Handler

(Handle WebSocket Disconnect: WebSocket Event Handler) {
    <Extract> the <connection-id> from the <event: connectionId>.
    <Extract> the <reason> from the <event: reason>.
    <Log> "WebSocket client disconnected" to the <console>.
    <Return> an <OK: status> for the <disconnection>.
}

Event properties:

Property Type Description
connectionId String Connection that closed
reason String Close reason (if provided)

Sending Messages

Broadcast to All Clients

Send a message to all connected WebSocket clients:

<Broadcast> the <message> to the <websocket>.

Messages are automatically serialized to JSON if they are objects.

Example: Broadcasting a New Message

(postMessage: Chat API) {
    <Extract> the <text> from the <request: body message>.
    <Create> the <message> with {
        text: <text>,
        createdAt: <now>
    }.
    <Store> the <message> into the <message-repository>.

    (* Push to all connected clients *)
    <Broadcast> the <message> to the <websocket>.

    <Return> a <Created: status> with <message>.
}

Complete Example: Real-Time Chat

Project Structure

WebChat/
├── openapi.yaml      # API contract
├── main.aro          # Application lifecycle
├── api.aro           # HTTP route handlers
├── websocket.aro     # WebSocket event handlers
└── templates/
    └── index.html    # Chat UI

main.aro

(Application-Start: Web Chat) {
    <Log> "Starting Web Chat..." to the <console>.
    <Start> the <http-server> with {}.
    <Log> "Server ready on http://localhost:8080" to the <console>.
    <Log> "WebSocket available on ws://localhost:8080/ws" to the <console>.
    <Keepalive> the <application> for the <events>.
    <Return> an <OK: status> for the <startup>.
}

(Application-End: Success) {
    <Log> "Shutting down..." to the <console>.
    <Stop> the <http-server> with {}.
    <Return> an <OK: status> for the <shutdown>.
}

api.aro

(homePage: Web Chat API) {
    <Transform> the <html> from the <template: index.html>.
    <Return> an <OK: status> with <html>.
}

(getMessages: Web Chat API) {
    <Retrieve> the <messages> from the <message-repository>.
    <Return> an <OK: status> with <messages>.
}

(postMessage: Web Chat API) {
    <Extract> the <body> from the <request: body>.
    <Extract> the <text: message> from the <body>.
    <Create> the <message: Message> with {
        message: <text>,
        createdAt: <now>
    }.
    <Store> the <message> into the <message-repository>.
    <Broadcast> the <message> to the <websocket>.
    <Return> a <Created: status> with <message>.
}

websocket.aro

(Handle WebSocket Connect: WebSocket Event Handler) {
    <Log> "WebSocket client connected" to the <console>.
    <Return> an <OK: status> for the <connection>.
}

(Handle WebSocket Disconnect: WebSocket Event Handler) {
    <Log> "WebSocket client disconnected" to the <console>.
    <Return> an <OK: status> for the <disconnection>.
}

openapi.yaml

openapi: 3.0.3
info:
  title: Web Chat API
  version: 1.0.0

paths:
  /home:
    get:
      operationId: homePage
  /messages:
    get:
      operationId: getMessages
    post:
      operationId: postMessage

Client JavaScript

// Connect to WebSocket
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${location.host}/ws`);

ws.onopen = () => {
    console.log('Connected');
};

ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    displayMessage(message);
};

ws.onclose = () => {
    console.log('Disconnected, reconnecting...');
    setTimeout(connect, 3000);
};

// Post via HTTP, receive updates via WebSocket
function postMessage(text) {
    fetch('/messages', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: 'message=' + encodeURIComponent(text)
    });
}

WebSocket Path

By default, WebSocket connections are accepted on /ws:

const ws = new WebSocket('ws://localhost:8080/ws');

Comparison: WebSocket vs TCP Socket

Feature WebSocket TCP Socket
Protocol WebSocket (RFC 6455) Raw TCP
Port HTTP port via Upgrade Dedicated port
Framing Message-based Stream-based
Browser support Native WebSocket API Not supported
Use case Web real-time apps Backend services
Handler pattern WebSocket Event Handler Socket Event Handler

Choose WebSocket for browser-based real-time features. Use TCP sockets for backend-to-backend communication or when you need a dedicated port.

Best Practices

Log Connection Events

(Handle WebSocket Connect: WebSocket Event Handler) {
    <Extract> the <id> from the <event: id>.
    <Extract> the <remote> from the <event: remoteAddress>.
    <Log> "Client connected: " to the <console>.
    <Log> <remote> to the <console>.
    <Return> an <OK: status> for the <connection>.
}

Handle Reconnection on Client

function connect() {
    const ws = new WebSocket('ws://localhost:8080/ws');

    ws.onclose = () => {
        console.log('Disconnected, reconnecting in 3s...');
        setTimeout(connect, 3000);
    };

    ws.onmessage = (event) => {
        handleMessage(JSON.parse(event.data));
    };
}

connect();

Use HTTP for Commands, WebSocket for Updates

The recommended pattern is:

  1. Client sends commands via HTTP POST
  2. Server broadcasts updates to all clients via WebSocket

This separates concerns and ensures reliable command delivery.

Notes

  • WebSocket uses SwiftNIO's WebSocket implementation
  • Available on macOS and Linux (not Windows)
  • JSON serialization is automatic for broadcast messages
  • Default path is /ws

Next Steps

Clone this wiki locally