Abstract Class: Room<State, Command, Env>
Defined in: server/room.ts:148
An authoritative game room: one Durable Object per room.
Event mode (the default) has no tick loop. The room wakes when a player connects, sends a command, drops or leaves, or when a timer fires. It runs the matching hook, saves what changed and sends each connected player their view of the state. Between events it hibernates while the WebSockets stay open, and rebuilds itself from storage on the next event.
State sync adds a simulation: set tickRate and the room calls onTick that many times a second while players are connected, sending each player only what changed in their view since the previous tick.
Hooks are synchronous and change this.state in place. The state must be JSON-serializable. A hook that calls reject() or throws leaves no trace: state, seats and timers go back to what they were.
Extends
DurableObject<Env>
Extended by
Type Parameters
| Type Parameter | Default type |
|---|---|
State | - |
Command | unknown |
Env | Cloudflare.Env |
Constructors
Constructor
new Room<State, Command, Env>(ctx, env): Room<State, Command, Env>;Defined in: server/room.ts:359
Parameters
| Parameter | Type |
|---|---|
ctx | DurableObjectState |
env | Env |
Returns
Room<State, Command, Env>
Overrides
DurableObject<Env>.constructor
Properties
| Property | Modifier | Type | Default value | Description | Defined in |
|---|---|---|---|---|---|
maxPlayers | protected | number | Number.POSITIVE_INFINITY | How many distinct players may hold a seat at once. | server/room.ts:150 |
maxMessageLength | protected | number | ... | Longest client frame accepted, in characters. | server/room.ts:153 |
seatTimeoutMs | protected | number | Number.POSITIVE_INFINITY | How long a disconnected player keeps their seat, in milliseconds. When it runs out, onLeave is called and the seat is given up, as if the player had left; a later connection from them is a new join. The default, Infinity, keeps seats until the player leaves or is removed, which is what a game played over days wants. A real-time room should set it, or closed tabs fill the room for good. | server/room.ts:163 |
maxCommandsPerSecond | protected | number | 30 | How many frames a second one player may send, and also how many they may send in a single burst. Frames over the limit are refused with rate_limited and never reach parseCommand. Infinity turns it off. | server/room.ts:170 |
matchQueue | protected | Queue<MatchResult> | undefined | undefined | Where finishMatch announces a finished match. Set it to a Queue binding: protected override matchQueue = this.env.MATCHES; | server/room.ts:176 |
matchBucket | protected | R2Bucket | undefined | undefined | Where finishMatch stores the replay it is given, if any. Set it to an R2 binding: protected override matchBucket = this.env.REPLAYS; | server/room.ts:182 |
rooms | protected | RoomNamespace | undefined | undefined | Where the other rooms are, for movePlayer and signal: the Durable Object namespace they live in, usually this room's own. protected override rooms = this.env.ROOMS; | server/room.ts:189 |
tickRate | protected | number | 0 | Simulation rate in ticks per second. 0, the default, is event mode: no tick loop. | server/room.ts:192 |
saveIntervalMs | protected | number | 5_000 | While the room is ticking, how often the state is written to storage, in milliseconds. It is also written whenever the simulation stops. A room that is torn down mid-simulation (a deploy, a crash) resumes from the last write. | server/room.ts:200 |
silentAfterMs | protected | number | 120_000 | While the room is ticking, how long a connection may send nothing, not a command and not a keepalive, before it is taken for dead and closed with CloseCode.Silent. The player is then away, as after any other drop: onDisconnect runs and their seat timeout starts. A room ticks for as long as someone is connected, and is billed for it. A connection whose other end is gone without the room being told would keep it awake for nobody, and this is what ends that. Tickflare's clients send a keepalive every 25 seconds, so the default of two minutes never closes one that is there; a client of your own has to send ping at least this often. 0 never closes a connection for silence. A room that is not ticking never does either: it is asleep. | server/room.ts:216 |
trackChanges | protected | boolean | true | Whether the kernel follows what each hook writes to the state, rather than turning the whole state into JSON after every hook to see what changed and to be able to undo it. On by default: a hook then costs what it touches, not what the state weighs. It means that inside a hook other than onTick, this.state and everything reached through it are proxies of the state's objects. They read and write like the objects themselves, with two exceptions: a proxy is not === to an object that did not come from the state, and structuredClone refuses one. plain(value) gives the object underneath for those. Reach the state through this.state in every hook, rather than through a reference kept from an earlier one: a change made through such a reference is not seen until the next tick or save, and is not undone if the hook is refused. Set to false to have every hook work on the plain state, at the old cost. | server/room.ts:236 |
Accessors
state
Get Signature
get protected state(): State;Defined in: server/room.ts:242
The authoritative state. Change it only inside hooks, through this.state: see trackChanges for what a hook is handed.
Returns
State
Set Signature
set protected state(value): void;Defined in: server/room.ts:247
Replaces the whole state. Like any other change, only inside hooks.
Parameters
| Parameter | Type |
|---|---|
value | State |
Returns
void
players
Get Signature
get protected players(): Player[];Defined in: server/room.ts:457
Everyone with a seat, in join order.
Returns
Player[]
seq
Get Signature
get protected seq(): number;Defined in: server/room.ts:464
Number of state changes so far.
Returns
number
tick
Get Signature
get protected tick(): number;Defined in: server/room.ts:469
Number of ticks run so far.
Returns
number
roomId
Get Signature
get protected roomId(): string | null;Defined in: server/room.ts:474
The id this room is reached under, once someone has connected through the gateway.
Returns
string | null
invited
Get Signature
get protected invited(): readonly string[] | null;Defined in: server/room.ts:483
The players a matchmaker reserved this room for, or null if it was not reserved. Only they can take a seat (players handed over from another room aside), so a match can start once all of them have joined.
Returns
readonly string[] | null
teams
Get Signature
get protected teams(): readonly (readonly string[])[] | null;Defined in: server/room.ts:491
The sides the matchmaker split the invited players into, each a list of player ids, or null if it did not: see Matchmaker.teams.
Returns
readonly (readonly string[])[] | null
Methods
initialState()
abstract protected initialState(): State;Defined in: server/room.ts:366
The state of a brand new room.
Returns
State
parseCommand()
abstract protected parseCommand(raw): Command;Defined in: server/room.ts:372
Turn an untrusted client payload into a command. Check everything here: raw is whatever the client sent. Throwing refuses the command.
Parameters
| Parameter | Type |
|---|---|
raw | unknown |
Returns
Command
onCommand()
abstract protected onCommand(player, command): void;Defined in: server/room.ts:375
Apply a command. Call reject() to refuse it.
Parameters
| Parameter | Type |
|---|---|
player | Player |
command | Command |
Returns
void
onJoin()
protected onJoin(_player): void;Defined in: server/room.ts:378
A player takes a seat for the first time. Call reject() to turn them away.
Parameters
| Parameter | Type |
|---|---|
_player | Player |
Returns
void
onDisconnect()
protected onDisconnect(_player): void;Defined in: server/room.ts:381
A seated player's last connection closed. They keep their seat.
Parameters
| Parameter | Type |
|---|---|
_player | Player |
Returns
void
onReconnect()
protected onReconnect(_player): void;Defined in: server/room.ts:384
A seated player connected again after being fully disconnected.
Parameters
| Parameter | Type |
|---|---|
_player | Player |
Returns
void
onLeave()
protected onLeave(_player): void;Defined in: server/room.ts:391
A player is giving up their seat: they asked to leave, or they stayed disconnected for longer than seatTimeoutMs. Tidy up after them here; they are removed when this returns. Call reject() to make them stay.
Parameters
| Parameter | Type |
|---|---|
_player | Player |
Returns
void
onEmpty()
protected onEmpty(): void;Defined in: server/room.ts:404
The last seat was given up: nobody is in the room any more, whichever way they went (they left, stayed away too long, were removed, or moved to another room). this.players is empty. Nothing ends a room by itself, so this is where to say what becomes of it: deleteRoom(), a timer that deletes it unless somebody comes back, or nothing, for a room that waits for its next players. Not called for a room nobody ever sat in.
It runs as a hook of its own, straight after the one that emptied the room. If it fails it alone is undone: the players are still gone.
Returns
void
onTimer()
protected onTimer(_name): void;Defined in: server/room.ts:407
A timer set with setTimer(name, ...) ran out.
Parameters
| Parameter | Type |
|---|---|
_name | string |
Returns
void
onTick()
protected onTick(_dt): void;Defined in: server/room.ts:413
Advance the simulation by one step of dt seconds (always 1 / tickRate). Runs tickRate times a second while the room is ticking.
Parameters
| Parameter | Type |
|---|---|
_dt | number |
Returns
void
shouldTick()
protected shouldTick(): boolean;Defined in: server/room.ts:422
Whether the simulation should be running. Checked after every event and every tick, so base it on the state, e.g. this.state.phase === "playing". While it returns false the room behaves as in event mode and can hibernate. The room does not tick while nobody is connected, unless ticksWhileEmpty() says so.
Returns
boolean
ticksWhileEmpty()
protected ticksWhileEmpty(): boolean;Defined in: server/room.ts:437
Whether the simulation keeps running while nobody is connected, for a match that has to reach its end whoever is watching. shouldTick() has to agree. Checked like shouldTick(), so base it on the state.
Give such a room an end. It is awake, and billed, for every second it ticks, and nobody is there to notice one that never stops: have onTick finish or delete the room when the match is over or has run too long. After a deploy or a crash it carries on from its last save within twenty seconds.
Returns
boolean
onHttp()
protected onHttp(_request, _player): Response | Promise<Response>;Defined in: server/room.ts:447
A plain HTTP request for this room, sent to {prefix}/{roomId}/... and already authenticated by the gateway. For reading from the room, e.g. a replay or a scoreboard: changes made to the state here are not saved or sent to anyone.
Parameters
| Parameter | Type |
|---|---|
_request | Request |
_player | Player |
Returns
Response | Promise<Response>
view()
protected view(_player): unknown;Defined in: server/room.ts:452
What player is allowed to see. Defaults to the whole state.
Parameters
| Parameter | Type |
|---|---|
_player | Player |
Returns
unknown
saveNow()
protected saveNow(): void;Defined in: server/room.ts:502
Have what the current hook changes written to storage as soon as the hook returns, instead of at the next save interval. Only matters while the room is ticking; outside the simulation every change is written at once. Use it when the hook also writes to the room's database itself, so the two cannot come apart. Call from inside a hook.
Returns
void
finishMatch()
protected finishMatch(result?, replay?): void;Defined in: server/room.ts:516
Announce that a match is over. result is yours to define (winner, scores, ...). It is put on matchQueue together with the room id and the players, for a queue consumer such as recordMatches to store. Pass a replay to have it written to matchBucket first; the message then carries its key. Call from inside a hook.
The announcement is saved with the room and sent after the hook has committed. If sending fails it is tried again on the room's next event.
Parameters
| Parameter | Type |
|---|---|
result? | Record<string, unknown> |
replay? | unknown |
Returns
void
movePlayer()
protected movePlayer(
playerId,
toRoomId,
carry?
): void;Defined in: server/room.ts:545
Hand a player over to another room, for worlds that are split across rooms. Their seat here is given up; once the other room has accepted them, their connection is closed with CloseCode.Moved and the client reconnects there by itself. carry (JSON) is delivered to the other room's onArrive: put in it whatever the player takes along. Remove their part of this room's state yourself. Call from inside a hook, and set rooms.
A player who is not connected at that moment is sent on when they next connect here.
Parameters
| Parameter | Type |
|---|---|
playerId | string |
toRoomId | string |
carry? | unknown |
Returns
void
handOver()
protected handOver(
roomId,
playerId,
carry
): Promise<void>;Defined in: server/room.ts:560
Deliver a player to another room, through roomsFor. If it throws, the handover is tried again on this room's next event.
Parameters
| Parameter | Type |
|---|---|
roomId | string |
playerId | string |
carry | unknown |
Returns
Promise<void>
onArrive()
protected onArrive(player, _carry): void;Defined in: server/room.ts:569
A player handed over by another room takes their seat. carry is what that room passed to movePlayer. Defaults to treating them as a new player.
Parameters
| Parameter | Type |
|---|---|
player | Player |
_carry | unknown |
Returns
void
send()
protected send(playerId, message): void;Defined in: server/room.ts:598
Send a message to one player, outside the state: an event they should hear about once ("you were hit", a result with more in it than an ack), or a stream you keep in step yourself. The client receives it in onMessage.
Messages go out when the event that sent them is over, in the order they were sent: after the state frame that event caused, or in a ticking room after the tick's patch. A message sent while handling a command can so arrive before the next tick shows the command's effect. A message to a player who is not connected is dropped, and so is everything a hook sent if that hook is refused. Unlike the state, nothing is sent again on reconnect: send what a returning player needs from onReconnect.
Parameters
| Parameter | Type |
|---|---|
playerId | string |
message | unknown |
Returns
void
broadcast()
protected broadcast(message): void;Defined in: server/room.ts:603
send to every player who is connected.
Parameters
| Parameter | Type |
|---|---|
message | unknown |
Returns
void
defer()
protected defer(task): void;Defined in: server/room.ts:616
Run task once this event is over, outside the hook: for talking to the world, which hooks cannot do because they are synchronous. Writing to D1, calling another Durable Object, fetch. Dropped if the hook is refused. Not awaited by anything and not retried: failures are logged. To change the state with what comes back, use act; to be sure it gets done across restarts, keep what is owed in the state and a timer that tries again.
Parameters
| Parameter | Type |
|---|---|
task | () => unknown |
Returns
void
act()
protected act(hook): Promise<ActOutcome>;Defined in: server/room.ts:626
Run hook like any other hook, from code that is not one: the end of a deferred task, a promise that resolved, an RPC method of your own. Its changes are committed and sent, or all taken back if it throws or calls reject(), which is what the returned outcome reports.
Parameters
| Parameter | Type |
|---|---|
hook | () => void |
Returns
Promise<ActOutcome>
signal()
protected signal(
roomId,
data,
options?
): void;Defined in: server/room.ts:643
Send a message to another room, which receives it in onSignal. It goes out once after this hook has committed. It is not saved and not retried: use it for things that are replaced by the next one anyway, such as a snapshot of what is near a border. Call from inside a hook, and set rooms.
With onlyIfWatched, a room that reports nobody connected is left alone for two seconds at a time, so an empty neighbour is not kept awake.
Parameters
| Parameter | Type |
|---|---|
roomId | string |
data | unknown |
options? | { onlyIfWatched?: boolean; } |
options.onlyIfWatched? | boolean |
Returns
void
relay()
protected relay(roomId, data): Promise<boolean>;Defined in: server/room.ts:656
Deliver a signal to another room, through roomsFor, resolving to whether anyone is connected there.
Parameters
| Parameter | Type |
|---|---|
roomId | string |
data | unknown |
Returns
Promise<boolean>
onSignal()
protected onSignal(_fromRoomId, _data): void;Defined in: server/room.ts:662
Another room sent this one a message with signal.
Parameters
| Parameter | Type |
|---|---|
_fromRoomId | string |
_data | unknown |
Returns
void
roomsFor()
protected roomsFor(_roomId): RoomNamespace | undefined;Defined in: server/room.ts:686
The namespace the room called roomId lives in. Defaults to rooms. Override it when handovers and signals go to rooms of more than one class, e.g. by the id's prefix.
Parameters
| Parameter | Type |
|---|---|
_roomId | string |
Returns
RoomNamespace | undefined
setTimer()
protected setTimer(name, delayMs): number;Defined in: server/room.ts:726
Run onTimer(name) once, delayMs from now. A timer with the same name is replaced. Returns the time it fires in epoch milliseconds, which you can put in the state to show a countdown. Call from inside a hook.
Timers are saved with the room, so they survive hibernation and restarts.
Parameters
| Parameter | Type |
|---|---|
name | string |
delayMs | number |
Returns
number
deleteRoom()
protected deleteRoom(): void;Defined in: server/room.ts:752
End this room for good. Shortly after the current event, everyone still connected is disconnected with CloseCode.Removed and the reason "room_deleted", and everything the room stored is deleted: its state, seats and timers, a lockstep recording, and any tables of your own. With nothing stored the Durable Object itself ceases to exist.
What the room still owes is delivered first: a finished match's result, a player on their way to another room. Until it is, the room stays. Work passed to defer() that is still running is given ten seconds to finish.
The room id is not used up. Someone who comes to it later finds a new, empty room. Call from inside a hook.
Returns
void
clearTimer()
protected clearTimer(name): void;Defined in: server/room.ts:757
Cancel a timer. Does nothing if it is not set. Call from inside a hook.
Parameters
| Parameter | Type |
|---|---|
name | string |
Returns
void
removePlayer()
protected removePlayer(playerId, reason?): void;Defined in: server/room.ts:766
Take a player's seat away and close their connection with CloseCode.Removed and reason. onLeave is not called: clean up their part of the state yourself. Call from inside a hook.
Parameters
| Parameter | Type | Default value |
|---|---|---|
playerId | string | undefined |
reason? | string | "removed" |
Returns
void
fetch()
fetch(request): Promise<Response>;Defined in: server/room.ts:775
Durable Object entry point for connections and HTTP requests from the gateway. Tickflare's: override onHttp instead.
Parameters
| Parameter | Type |
|---|---|
request | Request |
Returns
Promise<Response>
Overrides
DurableObject.fetch
webSocketMessage()
webSocketMessage(ws, message): Promise<void>;Defined in: server/room.ts:810
Durable Object entry point for incoming frames. Tickflare's: override parseCommand and onCommand instead.
Parameters
| Parameter | Type |
|---|---|
ws | WebSocket |
message | string | ArrayBuffer |
Returns
Promise<void>
Overrides
DurableObject.webSocketMessage
webSocketClose()
webSocketClose(
ws,
code,
reason
): Promise<void>;Defined in: server/room.ts:818
Durable Object entry point for closed connections. Tickflare's: override onDisconnect instead.
Parameters
| Parameter | Type |
|---|---|
ws | WebSocket |
code | number |
reason | string |
Returns
Promise<void>
Overrides
DurableObject.webSocketClose
webSocketError()
webSocketError(ws): Promise<void>;Defined in: server/room.ts:831
Durable Object entry point for broken connections. Tickflare's: override onDisconnect instead.
Parameters
| Parameter | Type |
|---|---|
ws | WebSocket |
Returns
Promise<void>
Overrides
DurableObject.webSocketError
alarm()
alarm(): Promise<void>;Defined in: server/room.ts:838
Durable Object entry point for the alarm that drives timers and retries. Tickflare's: override onTimer instead.
Returns
Promise<void>
Overrides
DurableObject.alarm