Task-oriented snippets for the games modality. Each assumes you’ve imported the
client:
import { Games } from "@clutchcall/sdk/games";
Join a room as a player
Construct with a playerId to take the player role.
const me = new Games({
relayHost: "relay.clutchcall.dev",
token: await fetchRoomToken("duel-42", "alice"),
roomId: "duel-42",
playerId: "alice",
});
Run the authoritative server
Omit playerId to take the authority role — it publishes state and reads every
player’s input.
const auth = new Games({ relayHost, token, roomId: "duel-42" }); // no playerId
Broadcast world state on a tick
Open the state publisher once, then write a self-contained snapshot per tick.
You drive the clock.
const state = await auth.publishState({ tickHz: 30 });
setInterval(() => state.write(serializeWorld(world)), 1000 / 30);
Render incoming state on the client
Subscribe to state; each callback is one snapshot. Latest-wins — never block on
an older one.
await me.subscribeState((bytes) => render(deserializeState(bytes)));
publishInput returns a publisher bound to this player’s id — no need to attach
your own from.
const input = await me.publishInput();
addEventListener("tick", () => input.write(serializeInput(localInput)));
One callback receives all players; the SDK decodes the from header for you.
await auth.subscribeInputs((playerId, bytes) => {
applyInput(playerId, deserializeInput(bytes));
});
Send a reliable chat event
Events ride a reliable, ordered stream — use them for anything that must not be
dropped.
const chat = await me.publishEvent({ channel: "chat" });
chat.write(serialize({ text: "gg" }));
Receive events with the sender id
subscribeEvents hands you the decoded sender and the payload.
await me.subscribeEvents({ channel: "chat" }, (fromPlayerId, bytes) => {
appendChat(fromPlayerId, deserialize(bytes));
});
Use multiple event channels
Each channel is an isolated reliable track — split chat from gameplay RPCs.
const chat = await me.publishEvent({ channel: "chat" });
const rpc = await me.publishEvent({ channel: "rpc.use_item" });
await me.subscribeEvents({ channel: "chat" }, onChat);
await me.subscribeEvents({ channel: "rpc.use_item" }, onItemRpc);
Emit a server-authoritative event
The authority can publish events too — they’re stamped from = "_authority", so
clients can trust them as coming from the server.
const settle = await auth.publishEvent({ channel: "match.result" });
settle.write(serialize({ winner: "alice", score: [3, 1] }));
Override priority on a write
state/input default to 100, events to 50. Bump a critical event above the
rest of its lane.
const events = await me.publishEvent({ channel: "chat" });
events.write(serialize(urgentLine), 90); // higher than default 50
Track connection state
Pass onState to watch the link. The session reconnects and replays your
channels automatically — no manual re-subscribe.
const me = new Games({
relayHost, token, roomId: "duel-42", playerId: "alice",
onState: (s) => {
if (s === 1) showOnline();
if (s === 2) showReconnecting();
},
});
Stop one channel without leaving the room
Every subscribe* returns a handle you can close() while keeping other
channels open.
const sub = await me.subscribeEvents({ channel: "chat" }, onChat);
sub.close(); // stop receiving chat; input/state stay live
Leave the room
close() tears down the shared session and every channel on it.
Validate a player id before joining
Player ids must encode to ≤ 255 UTF-8 bytes — the wire header is one length
byte. Guard untrusted ids client-side.
if (new TextEncoder().encode(playerId).length > 255) {
throw new Error("player id too long");
}
Picking between channels: if dropping the message is fine because a newer one is
coming, use state or input (datagram, latest-wins). If a player would
notice it missing, use an event (reliable, ordered).