Skip to Content
Game ServerRoom State

Managing Room State

Room state is a single document shared by everyone in a room — match phase, round timer, room settings. It is ephemeral: never persisted, and deleted when the last player leaves.

On the server, every room state read includes two fields the runtime maintains: { roomId: string, $users: string[] } — for example { roomId: "roomA", $users: ["account1", "account2"] }. $users is the room’s current membership, updated as users join and leave, and it reaches clients too. roomId does not — it is added on the server read and is not part of the stored state, so useRoomState() and subscribeRoomState never see it. On the client, take the room id from useGameServer().currentRoomId.

Server API

  • $room.getRoomState(paths?: string[]): Promise<Object>: The room’s state, or only the dot-notation paths you ask for — see Partial Fetch.
  • $room.updateRoomState(state: Object, opts?: { returnState?: boolean }): Promise<Object | null>: Shallow-merges state into the room’s state; a top-level undefined deletes that field. Resolves with the patch you passed — on a hot path such as a $roomTick, pass { returnState: false }; see Fire-and-forget writes.
  • $room.clearRoomState(fields?: string[]): Promise<Object>: Deletes top-level fields. Wiping preserves roomId and $users.

Partial Fetch

paths follows the global state rules, so a hot-path handler such as a $roomTick can read just the fields it needs:

server/src/server.ts
const state = await $room.getRoomState(['session.status']); // state === { roomId: 'roomA', session: { status: 'running' }, $users: [...] }

Two room-specific differences: roomId and $users are always included regardless of paths, and paths === [] returns just { roomId, $users } (global state returns {}).

Client API

  • server.subscribeRoomState(roomId: string, (state: any) => {}): UnsubscribeFunction — roomId is required.
  • const roomState = useRoomState() — always the current room; subscribes for you.

Both need the Room Server connection — gate on rsConnected (Joining a Room). Callbacks are throttled full snapshots, not one callback per write: the server coalesces changes on a 30 ms trailing edge and sends the whole state each time, so several rapid writes can arrive as a single callback.


Example: Room Counter

A counter shared by everyone in the room. The increment is a read-modify-write on state the whole room shares, so it runs inside $lock under a room-scoped key.

server/src/server.ts
export class Server { async countup() { // One lock per room: two clicks that arrive together would otherwise both // read the same count and one of the increments would be lost. return $lock(`room-counter:${$sender.roomId}`, async () => { const currentState = await $room.getRoomState(["count"]); const newCount = (currentState.count || 0) + 1; await $room.updateRoomState({ count: newCount }); return { count: newCount }; }); } }
src/App.tsx
import { useGameServer, useRoomState } from "@agent8/gameserver"; import { useEffect } from "react"; export default function CountUp() { const { connected, server, joinRoom, rsConnected } = useGameServer(); const roomState = useRoomState(); // Join as soon as the game server is reachable. useEffect(() => { if (!connected) return; joinRoom("room1").catch(console.error); }, [connected, joinRoom]); if (!rsConnected) return <p>Joining room...</p>; return ( <div> <p>Count: {roomState.count || 0}</p> <button onClick={() => server.remoteFunction("countup")}> Increase Count </button> </div> ); }
Last updated on