alepha@docs:~/framework/docs/reference/primitives$
cat $room.md | pretty
1 min read

#$room

#Import

typescript
1import { $room } from "alepha/websocket";

#Overview

Defines a stateful WebSocket room - the home for an authoritative, optionally tick-driven simulation.

Where $websocket is a stateless, per-message pub/sub handler, $room holds in-memory state across messages and drives a server-side tick loop. It is the primitive a real-time game server is built on:

  • tickHz > 0 → an authoritative simulation room. The loop runs only while a socket is connected (an empty room costs nothing), calling onTick(room, dt) every 1000/tickHz ms. Inside a tick you read room.state, iterate room.connections, and push per-recipient frames with room.send(id, msg) or room.broadcast(msg).
  • tickHz omitted → a headless coordinator addressed by id and reached through server-side methods. This is how you model cross-room party state or a single-owner presence lease.

Same code runs on Node (one process, real timers) and on Cloudflare (one Durable Object per channelPath:roomId, the loop alive while the socket is).

#Examples

An authoritative 20Hz world

typescript
 1class GameServer { 2  world = $channel({ 3    path: "/ws/world", 4    schema: { in: serverMsg, out: clientIntent }, 5  }); 6  7  room = $room({ 8    channel: this.world, 9    tickHz: 20,10    state: () => new World(),11    onJoin: (room, conn) => room.state.addPlayer(conn.id),12    onMessage: (room, conn, intent) => room.state.enqueue(conn.id, intent),13    onTick: (room, dt) => {14      room.state.step(dt);15      for (const conn of room.connections)16        room.send(conn.id, room.state.viewFor(conn.id));17    },18    onLeave: (room, conn) => room.state.removePlayer(conn.id),19    onEmpty: (room) => room.state.persist(),20  });21}

A headless coordinator (party-wide state)

typescript
 1class Coordinator { 2  party = $channel({ path: "/ws/party", schema: { in: any, out: any } }); 3  4  session = $room({ 5    channel: this.party, 6    state: () => ({ switches: {} as Record<string, boolean> }), 7    methods: { 8      setSwitch: (room, id: string, value: boolean) => { 9        room.state.switches[id] = value;10      },11      snapshot: (room) => room.state.switches,12    },13  });14 15  async flip(partyId: string, id: string) {16    await this.session.call(partyId, "setSwitch", id, true);17  }18}