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)));

Send player input each tick

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)));

Fan in every player’s input on the server

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.
await me.close();

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).