alepha@docs:~/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 {@link $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({ path: "/ws/world", schema: { in: serverMsg, out: clientIntent } }); 3  4  room = $room({ 5    channel: this.world, 6    tickHz: 20, 7    state: () => new World(), 8    onJoin:    (room, conn) => room.state.addPlayer(conn.id), 9    onMessage: (room, conn, intent) => room.state.enqueue(conn.id, intent),10    onTick:    (room, dt) => {11      room.state.step(dt);12      for (const conn of room.connections) room.send(conn.id, room.state.viewFor(conn.id));13    },14    onLeave:   (room, conn) => room.state.removePlayer(conn.id),15    onEmpty:   (room) => room.state.persist(),16  });17}

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) => { room.state.switches[id] = value; }, 9      snapshot:  (room) => room.state.switches,10    },11  });12 13  async flip(partyId: string, id: string) {14    await this.session.call(partyId, "setSwitch", id, true);15  }16}