Multiplayer

Lockstep from a reducer.

Use this for turn-based and event-driven games. Loki sequences actions, commits only from the current host, and tracks stateVersion.

Contract

  1. JSON-compatible state and actions. Numbers must be finite safe integers.
  2. reduce(state, action, context) must be synchronous, deterministic, and fast (budget 50 ms). No I/O or rendering inside reduce.
  3. Only the current host runs reduce.
  4. Subscribe to snapshots for state, members, host, and connection.
  5. Call dispatch(action) only while connection === "connected".
const room = client.createSynchronizedRoom<State, Action>({
  initialState: { board: emptyBoard, turn: "white" },
  reduce(state, action, context) {
    if (action.type === "move") return applyMove(state, action, context.senderId);
    return state;
  },
});

const created = await room.create();
room.subscribe((snapshot) => {
  render(snapshot.state, snapshot.members, snapshot.connection);
});
await room.dispatch({ type: "move", from: "e2", to: "e4" });

Shared methods: create(), join({ inviteCode }), dispatch(action), leave(), reconnect().

Connection

connectionWhat to do
connectedDispatch is allowed
suspendedPage hidden or offline. Lock input. Do not leave.
reconnecting / resynchronizingWait for a snapshot. Host may have changed.
leave_failedRetry leave() or close()
room_closedTerminal. Return to lobby.

Coming back replaces the socket and replays unresolved actions with the same (senderId, actionId). Do not resend under a new ID. indeterminate is unknown, not proof of failure.

Prefer createSynchronizedRoom() over low-level sendAction() / sendHostState().