Abstract Class: LockstepRoom<Input, Env>
Defined in: server/lockstep.ts:69
A lockstep room. It never runs the game: it validates inputs, orders them into turns at tickRate turns a second, records every turn in which something happened, and sends all players the same stream. Players coming and going are part of that stream, so every simulation sees them at the same turn.
The recording is served at {prefix}/{roomId}/replay. Clients read it when they (re)connect, to catch up, and it doubles as the match replay.
By default the room closes the turns itself, on a timer, and so is awake and billed for as long as the match lasts. Set turnClock = "players" and it sleeps between turns instead: see turnClock.
If you override onJoin, onDisconnect or onReconnect, call super so the event still reaches the turn stream. A seat that is given up, whether the player left, timed out or was removed with removePlayer, is announced to the stream for you.
Extends
Room<LockstepState<Input>,LockstepCommand<Input>,Env>
Type Parameters
| Type Parameter | Default type |
|---|---|
Input | - |
Env | Cloudflare.Env |
Constructors
Constructor
new LockstepRoom<Input, Env>(ctx, env): LockstepRoom<Input, Env>;Defined in: server/room.ts:359
Parameters
| Parameter | Type |
|---|---|
ctx | DurableObjectState |
env | Env |
Returns
LockstepRoom<Input, Env>
Inherited from
Properties
| Property | Modifier | Type | Default value | Description | Overrides | Inherited from | Defined in |
|---|---|---|---|---|---|---|---|
tickRate | protected | number | 10 | Turns per second. | Room.tickRate | - | server/lockstep.ts:75 |
maxInputsPerTurn | protected | number | 32 | Inputs one player may send within a single turn. More are refused. | - | - | server/lockstep.ts:78 |
turnClock | protected | "players" | "room" | "room" | Who keeps time. "room": the room closes a turn every 1 / tickRate seconds on a timer of its own. It is awake for the whole match. "players": the room has no timer. LockstepClient asks it for each turn as it falls due, one player's client at a time, and the room closes as many turns as its own clock says have passed, never more: a player can neither speed the game up nor hold it back while anyone else is connected. Between requests the room is asleep, which is most of the time. Orders take effect as soon as they do with "room". Two things differ: the game stands still while nobody is connected to ask, and after a restart it skips ahead by up to saveIntervalMs of empty turns instead of going back by as much. | - | - | server/lockstep.ts:96 |
snapshotIntervalMs | protected | number | 60_000 | How often the room takes a snapshot of the simulation from its players, so that someone who joins, reloads or reconnects late starts from it and not from turn 1. 0 keeps none. The room has no simulation of its own. A snapshot is what one player's client saved, with save, at a turn every client agrees on, and it is kept once the hash it came with is the one another connected player reported for that turn; a player who is alone is taken at their word. The client that loads it hashes the result again before trusting it. Clients whose simulation has no save, load and hash send none and load none, and catch up from the recording as before. Only the latest is kept. The recording is not shortened: it is still the whole match. | - | - | server/lockstep.ts:114 |
maxSnapshotLength | protected | number | 8_000_000 | The longest snapshot the room keeps, in characters of JSON. A longer one is refused. | - | - | server/lockstep.ts:117 |
maxPlayers | protected | number | Number.POSITIVE_INFINITY | How many distinct players may hold a seat at once. | - | Room.maxPlayers | server/room.ts:150 |
maxMessageLength | protected | number | ... | Longest client frame accepted, in characters. | - | Room.maxMessageLength | 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. | - | Room.seatTimeoutMs | 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. | - | Room.maxCommandsPerSecond | 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; | - | Room.matchQueue | 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; | - | Room.matchBucket | 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; | - | Room.rooms | server/room.ts:189 |
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. | - | Room.saveIntervalMs | 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. | - | Room.silentAfterMs | 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. | - | Room.trackChanges | 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
Inherited from
players
Get Signature
get protected players(): Player[];Defined in: server/room.ts:457
Everyone with a seat, in join order.
Returns
Player[]
Inherited from
seq
Get Signature
get protected seq(): number;Defined in: server/room.ts:464
Number of state changes so far.
Returns
number
Inherited from
tick
Get Signature
get protected tick(): number;Defined in: server/room.ts:469
Number of ticks run so far.
Returns
number
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
Methods
parseInput()
abstract protected parseInput(raw): Input;Defined in: server/lockstep.ts:145
Turn an untrusted client payload into an input. Every client will feed what this returns to its simulation, so reject anything malformed here.
Parameters
| Parameter | Type |
|---|---|
raw | unknown |
Returns
Input
onDesync()
protected onDesync(_turn): void;Defined in: server/lockstep.ts:148
Two players reported different hashes for turn: their simulations have diverged.
Parameters
| Parameter | Type |
|---|---|
_turn | number |
Returns
void
initialState()
protected initialState(): LockstepState<Input>;Defined in: server/lockstep.ts:150
The state of a brand new room.
Returns
LockstepState<Input>
Overrides
parseCommand()
protected parseCommand(raw): LockstepCommand<Input>;Defined in: server/lockstep.ts:155
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
LockstepCommand<Input>
Overrides
shouldTick()
protected shouldTick(): boolean;Defined in: server/lockstep.ts:264
The room keeps time only when it is asked to: with players asking, it has no simulation to wake for.
Returns
boolean
Overrides
onCommand()
protected onCommand(player, command): void;Defined in: server/lockstep.ts:268
Apply a command. Call reject() to refuse it.
Parameters
| Parameter | Type |
|---|---|
player | Player |
command | LockstepCommand<Input> |
Returns
void
Overrides
onJoin()
protected onJoin(player): void;Defined in: server/lockstep.ts:460
A player takes a seat for the first time. Call reject() to turn them away.
Parameters
| Parameter | Type |
|---|---|
player | Player |
Returns
void
Overrides
removePlayer()
protected removePlayer(playerId, reason?): void;Defined in: server/lockstep.ts:471
Every way of losing a seat ends here, so every simulation hears about it.
Parameters
| Parameter | Type |
|---|---|
playerId | string |
reason? | string |
Returns
void
Overrides
onDisconnect()
protected onDisconnect(player): void;Defined in: server/lockstep.ts:478
A seated player's last connection closed. They keep their seat.
Parameters
| Parameter | Type |
|---|---|
player | Player |
Returns
void
Overrides
onReconnect()
protected onReconnect(player): void;Defined in: server/lockstep.ts:482
A seated player connected again after being fully disconnected.
Parameters
| Parameter | Type |
|---|---|
player | Player |
Returns
void
Overrides
onTick()
protected onTick(): void;Defined in: server/lockstep.ts:492
Closes the turn: whatever arrived since the last one becomes this turn's entries. A turn with something in it is one row in the recording, written before any player is sent it; #resume is what lets the room itself be saved on its usual schedule instead of with every turn.
Returns
void
Overrides
view()
protected view(): LockstepView<Input>;Defined in: server/lockstep.ts:508
What player is allowed to see. Defaults to the whole state.
Returns
LockstepView<Input>
Overrides
onHttp()
protected onHttp(request, player): Response | Promise<Response>;Defined in: server/lockstep.ts:530
GET …/replay is the recording, or the end of it for a client that is catching up. POST …/snapshot is a player's client sending a snapshot. If you override this, pass on what is not yours with super.onHttp.
Parameters
| Parameter | Type |
|---|---|
request | Request |
player | Player |
Returns
Response | Promise<Response>
Overrides
finishMatch()
protected finishMatch(result?): void;Defined in: server/lockstep.ts:542
Announces the end of the match and, if matchBucket is set, stores the recording as its replay.
Parameters
| Parameter | Type |
|---|---|
result? | Record<string, unknown> |
Returns
void
Overrides
replay()
protected replay(): Replay<Input>;Defined in: server/lockstep.ts:547
The recording so far.
Returns
Replay<Input>
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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>
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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>
Inherited from
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
Inherited from
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>
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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
Inherited from
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>
Inherited from
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>
Inherited from
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>
Inherited from
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>
Inherited from
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>