Skip to content

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 ParameterDefault type
State-
Commandunknown
EnvCloudflare.Env

Constructors ​

Constructor ​

ts
new Room<State, Command, Env>(ctx, env): Room<State, Command, Env>;

Defined in: server/room.ts:359

Parameters ​

ParameterType
ctxDurableObjectState
envEnv

Returns ​

Room<State, Command, Env>

Overrides ​

DurableObject<Env>.constructor

Properties ​

PropertyModifierTypeDefault valueDescriptionDefined in
maxPlayersprotectednumberNumber.POSITIVE_INFINITYHow many distinct players may hold a seat at once.server/room.ts:150
maxMessageLengthprotectednumber...Longest client frame accepted, in characters.server/room.ts:153
seatTimeoutMsprotectednumberNumber.POSITIVE_INFINITYHow 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
maxCommandsPerSecondprotectednumber30How 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
matchQueueprotectedQueue<MatchResult> | undefinedundefinedWhere finishMatch announces a finished match. Set it to a Queue binding: protected override matchQueue = this.env.MATCHES;server/room.ts:176
matchBucketprotectedR2Bucket | undefinedundefinedWhere 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
roomsprotectedRoomNamespace | undefinedundefinedWhere 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
tickRateprotectednumber0Simulation rate in ticks per second. 0, the default, is event mode: no tick loop.server/room.ts:192
saveIntervalMsprotectednumber5_000While 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
silentAfterMsprotectednumber120_000While 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
trackChangesprotectedbooleantrueWhether 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 ​

ts
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 ​

ts
set protected state(value): void;

Defined in: server/room.ts:247

Replaces the whole state. Like any other change, only inside hooks.

Parameters ​
ParameterType
valueState
Returns ​

void


players ​

Get Signature ​

ts
get protected players(): Player[];

Defined in: server/room.ts:457

Everyone with a seat, in join order.

Returns ​

Player[]


seq ​

Get Signature ​

ts
get protected seq(): number;

Defined in: server/room.ts:464

Number of state changes so far.

Returns ​

number


tick ​

Get Signature ​

ts
get protected tick(): number;

Defined in: server/room.ts:469

Number of ticks run so far.

Returns ​

number


roomId ​

Get Signature ​

ts
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 ​

ts
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 ​

ts
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() ​

ts
abstract protected initialState(): State;

Defined in: server/room.ts:366

The state of a brand new room.

Returns ​

State


parseCommand() ​

ts
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 ​

ParameterType
rawunknown

Returns ​

Command


onCommand() ​

ts
abstract protected onCommand(player, command): void;

Defined in: server/room.ts:375

Apply a command. Call reject() to refuse it.

Parameters ​

ParameterType
playerPlayer
commandCommand

Returns ​

void


onJoin() ​

ts
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 ​

ParameterType
_playerPlayer

Returns ​

void


onDisconnect() ​

ts
protected onDisconnect(_player): void;

Defined in: server/room.ts:381

A seated player's last connection closed. They keep their seat.

Parameters ​

ParameterType
_playerPlayer

Returns ​

void


onReconnect() ​

ts
protected onReconnect(_player): void;

Defined in: server/room.ts:384

A seated player connected again after being fully disconnected.

Parameters ​

ParameterType
_playerPlayer

Returns ​

void


onLeave() ​

ts
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 ​

ParameterType
_playerPlayer

Returns ​

void


onEmpty() ​

ts
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() ​

ts
protected onTimer(_name): void;

Defined in: server/room.ts:407

A timer set with setTimer(name, ...) ran out.

Parameters ​

ParameterType
_namestring

Returns ​

void


onTick() ​

ts
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 ​

ParameterType
_dtnumber

Returns ​

void


shouldTick() ​

ts
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() ​

ts
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() ​

ts
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 ​

ParameterType
_requestRequest
_playerPlayer

Returns ​

Response | Promise<Response>


view() ​

ts
protected view(_player): unknown;

Defined in: server/room.ts:452

What player is allowed to see. Defaults to the whole state.

Parameters ​

ParameterType
_playerPlayer

Returns ​

unknown


saveNow() ​

ts
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() ​

ts
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 ​

ParameterType
result?Record<string, unknown>
replay?unknown

Returns ​

void


movePlayer() ​

ts
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 ​

ParameterType
playerIdstring
toRoomIdstring
carry?unknown

Returns ​

void


handOver() ​

ts
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 ​

ParameterType
roomIdstring
playerIdstring
carryunknown

Returns ​

Promise<void>


onArrive() ​

ts
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 ​

ParameterType
playerPlayer
_carryunknown

Returns ​

void


send() ​

ts
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 ​

ParameterType
playerIdstring
messageunknown

Returns ​

void


broadcast() ​

ts
protected broadcast(message): void;

Defined in: server/room.ts:603

send to every player who is connected.

Parameters ​

ParameterType
messageunknown

Returns ​

void


defer() ​

ts
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 ​

ParameterType
task() => unknown

Returns ​

void


act() ​

ts
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 ​

ParameterType
hook() => void

Returns ​

Promise<ActOutcome>


signal() ​

ts
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 ​

ParameterType
roomIdstring
dataunknown
options?{ onlyIfWatched?: boolean; }
options.onlyIfWatched?boolean

Returns ​

void


relay() ​

ts
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 ​

ParameterType
roomIdstring
dataunknown

Returns ​

Promise<boolean>


onSignal() ​

ts
protected onSignal(_fromRoomId, _data): void;

Defined in: server/room.ts:662

Another room sent this one a message with signal.

Parameters ​

ParameterType
_fromRoomIdstring
_dataunknown

Returns ​

void


roomsFor() ​

ts
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 ​

ParameterType
_roomIdstring

Returns ​

RoomNamespace | undefined


setTimer() ​

ts
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 ​

ParameterType
namestring
delayMsnumber

Returns ​

number


deleteRoom() ​

ts
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() ​

ts
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 ​

ParameterType
namestring

Returns ​

void


removePlayer() ​

ts
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 ​

ParameterTypeDefault value
playerIdstringundefined
reason?string"removed"

Returns ​

void


fetch() ​

ts
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 ​

ParameterType
requestRequest

Returns ​

Promise<Response>

Overrides ​

DurableObject.fetch


webSocketMessage() ​

ts
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 ​

ParameterType
wsWebSocket
messagestring | ArrayBuffer

Returns ​

Promise<void>

Overrides ​

DurableObject.webSocketMessage


webSocketClose() ​

ts
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 ​

ParameterType
wsWebSocket
codenumber
reasonstring

Returns ​

Promise<void>

Overrides ​

DurableObject.webSocketClose


webSocketError() ​

ts
webSocketError(ws): Promise<void>;

Defined in: server/room.ts:831

Durable Object entry point for broken connections. Tickflare's: override onDisconnect instead.

Parameters ​

ParameterType
wsWebSocket

Returns ​

Promise<void>

Overrides ​

DurableObject.webSocketError


alarm() ​

ts
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

MIT licence. Tickflare is an independent project and is not affiliated with Cloudflare.