Skip to content

WebSocket Handlers

WebSocket ingress rules map a URL path to a JavaScript script that handles real-time, bidirectional communication. Unlike HTTP handlers (which get a new context per request), WebSocket scripts maintain a persistent GraalVM context for the lifetime of the connection.

How It Works

  1. A client connects to a WebSocket URL matching an ingress rule (e.g., ws://host/ws/chat)
  2. The engine loads the linked script and creates a GraalVM context that stays open
  3. The script's onOpen(session) export is called
  4. For each incoming message, onMessage(session, message) is called
  5. When the client disconnects, onClose(session) is called
  6. The GraalVM context is closed

INFO

WebSocket routes must start with /ws/. Clients connect directly — there is no /backend prefix like HTTP routes.

Required Exports

A WebSocket handler script must export three lifecycle functions:

javascript
export function onOpen(session) {
  // Called when a client connects
}

export function onMessage(session, message) {
  // Called for each incoming text message
}

export function onClose(session) {
  // Called when the client disconnects
}

All three exports are required. The session parameter is a WebSocketSession object.

WARNING

The Results global is not available in WebSocket scripts. These handlers don't return HTTP responses — they communicate via session.send().

Basic Echo Server

javascript
export function onOpen(session) {
  session.send(JSON.stringify({ type: "connected", id: session.id }));
}

export function onMessage(session, message) {
  session.send(message); // echo back
}

export function onClose(session) {
  console.log("Client disconnected:", session.id);
}

Working with Rooms

Rooms enable group communication. Sessions can join multiple rooms simultaneously.

javascript
export function onOpen(session) {
  const roomName = session.getParameter("room") || "general";
  session.setAttribute("room", roomName);
  session.room(roomName).join();

  session.send(JSON.stringify({
    type: "joined",
    room: roomName,
    members: session.room(roomName).members()
  }));

  session.room(roomName).broadcastExcept(JSON.stringify({
    type: "user-joined",
    id: session.id
  }));
}

export function onMessage(session, message) {
  const roomName = session.getAttribute("room");
  session.room(roomName).broadcastExcept(message);
}

export function onClose(session) {
  const roomName = session.getAttribute("room");
  session.room(roomName).broadcastExcept(JSON.stringify({
    type: "user-left",
    id: session.id
  }));
  // Room leave is automatic after onClose returns
}

Room Key Points

  • Rooms are lazy — created on first join(), removed when empty
  • room.broadcast(msg) — sends to everyone in the room, including the sender
  • room.broadcastExcept(msg) — sends to everyone except the sender
  • room.members() — returns the count of current room members
  • Cleanup is automatic — room memberships are removed after onClose returns

Per-Connection State

Use session.setAttribute() / session.getAttribute() for state that lives as long as the connection:

javascript
export function onOpen(session) {
  const token = session.getParameter("token");
  const user = AuthService.validateToken(token);

  if (!user) {
    session.send(JSON.stringify({ type: "error", message: "Unauthorized" }));
    session.close();
    return;
  }

  session.setAttribute("user", user);
  session.setAttribute("connectedAt", Date.now());
}

export function onMessage(session, message) {
  const user = session.getAttribute("user");
  // user is available for the lifetime of this connection
}

Query Parameters

Pass data during the WebSocket handshake via query parameters:

ws://your-host/ws/game?room=arena1&player=alice
javascript
export function onOpen(session) {
  const room = session.getParameter("room");
  const player = session.getParameter("player");
  // ...
}

Message Protocol Pattern

A common pattern is to use JSON messages with a type field for routing:

javascript
export function onMessage(session, raw) {
  const msg = JSON.parse(raw);

  switch (msg.type) {
    case "chat":
      handleChat(session, msg);
      break;
    case "move":
      handleMove(session, msg);
      break;
    case "ping":
      session.send(JSON.stringify({ type: "pong" }));
      break;
    default:
      session.send(JSON.stringify({ type: "error", message: "Unknown type" }));
  }
}

function handleChat(session, msg) {
  session.room("lobby").broadcastExcept(JSON.stringify({
    type: "chat",
    from: session.getAttribute("username"),
    text: msg.text,
    timestamp: Date.now()
  }));
}

function handleMove(session, msg) {
  // Game logic...
}

Full Example: Multi-Room Chat

javascript
export function onOpen(session) {
  const username = session.getParameter("user") || "anon-" + session.id.slice(0, 6);
  session.setAttribute("username", username);
  session.setAttribute("rooms", []);

  session.send(JSON.stringify({
    type: "welcome",
    username,
    id: session.id
  }));
}

export function onMessage(session, raw) {
  const msg = JSON.parse(raw);

  switch (msg.type) {
    case "join": {
      const room = session.room(msg.room);
      room.join();
      const rooms = session.getAttribute("rooms");
      rooms.push(msg.room);
      session.setAttribute("rooms", rooms);

      session.send(JSON.stringify({
        type: "joined",
        room: msg.room,
        members: room.members()
      }));
      room.broadcastExcept(JSON.stringify({
        type: "user-joined",
        room: msg.room,
        user: session.getAttribute("username")
      }));
      break;
    }

    case "leave": {
      session.room(msg.room).broadcastExcept(JSON.stringify({
        type: "user-left",
        room: msg.room,
        user: session.getAttribute("username")
      }));
      session.room(msg.room).leave();
      break;
    }

    case "message": {
      session.room(msg.room).broadcastExcept(JSON.stringify({
        type: "message",
        room: msg.room,
        from: session.getAttribute("username"),
        text: msg.text,
        timestamp: Date.now()
      }));
      break;
    }
  }
}

export function onClose(session) {
  const username = session.getAttribute("username");
  const rooms = session.getAttribute("rooms") || [];

  for (const roomName of rooms) {
    session.room(roomName).broadcastExcept(JSON.stringify({
      type: "user-left",
      room: roomName,
      user: username
    }));
  }
  // All room memberships are cleaned up automatically
}

DieselEngine Scripting Documentation