The games modality lives in its own subpath. Import the Games client plus its publisher/subscription handles from there.
import {
  Games,
  StatePublisher,
  FromPublisher,
  GamesSubscription,
} from "@clutchcall/sdk/games";
The TypeScript and Python surfaces mirror each other method-for-method, differing only in idiomatic case (publishStatepublish_state) and keyword vs options style. Both sit on the same MoQT substrate, so a TS player and a Python authority interoperate on the wire.

Games — the client

One Games instance per (room, player) — or (room, server) for the authority. The session is opened lazily on the first publish or subscribe and reused for every channel.
const games = new Games({
  token:    await fetchRoomToken("duel-42", "alice"),
  roomId:   "duel-42",
  playerId: "alice",          // omit for the authoritative server
  relayHost: "relay.clutchcall.dev",
  onState:  (state, reason) => console.log("conn", state, reason),
});
token
string
required
Bearer token for the relay session, scoped to (tenant, room, player?).
roomId
string
required
The room every channel binds to. All three channels live under game/<roomId>/….
playerId
string
Player identity. Omit for the authoritative server. Required to publishInput or publishEvent — it becomes the from header on every frame.
relayHost
string
Relay hostname. Defaults to the platform relay.
onState
(state, reason?) => void
Connection-state callback. state is a ConnectionState (0 Connecting · 1 Connected · 2 Reconnecting · 3 Closed · 4 Failed).
webTransport
WebTransportFactory
Inject a custom WebTransport factory (for a Node polyfill or tests). Optional.
The constructor throws (Error / GamesError) if token or roomId/room_id is missing.

Namespace accessors

Read-only helpers if you need to inspect or interop with the raw tracks:
games.stateNs;            // "game/duel-42/state"
games.inputNs;           // "game/duel-42/input"
games.eventNs("chat");   // "game/duel-42/event/chat"

state — server → all

publishState(args?) → StatePublisher

Authority-only. Opens the state track and returns a StatePublisher. State frames carry no from header (the source is always the authority) and ride the datagram lane at priority 100.
const state: StatePublisher = await games.publishState({ tickHz: 30 });
tickHz
number
Tick-rate hint for the relay’s admission control and the recorder. Does not pace your writes — you drive the loop. Optional.

subscribeState(cb) → GamesSubscription

Player-side. Subscribes to the authority’s state and invokes your callback with each snapshot’s raw bytes. Returns a GamesSubscription you can close().
const sub = await games.subscribeState((bytes: Uint8Array) => {
  render(deserializeState(bytes));
});

StatePublisher

write(stateBytes, priority?)
void
Publish one state snapshot. priority defaults to 100. Timestamps are stamped for you from a monotonic clock.
close()
void
Close the state track.

input — each player → server

publishInput() → FromPublisher

Player-only. Returns a FromPublisher that prefixes every frame with this client’s playerId. Rides the datagram lane at priority 100. Throws if the client has no playerId.
const input: FromPublisher = await games.publishInput();
addEventListener("tick", () => input.write(serializeInput(localInput)));

subscribeInputs(cb) → GamesSubscription

Authority-side. Receives every player’s input on one callback. The SDK decodes the from header, so you get (playerId, bytes) per frame — no per-player subscription bookkeeping.
await games.subscribeInputs((playerId: string, bytes: Uint8Array) => {
  applyInput(playerId, deserializeInput(bytes));
});
Malformed input frames (bad from header) are dropped silently rather than throwing into your callback, so one bad packet can’t stall the tick loop. The relay logs them as wire-format rejects.

event — any → any (reliable)

publishEvent(args) → FromPublisher

Either role. Opens a reliable, ordered event channel and returns a FromPublisher. Events ride a subgroup stream at priority 50 — guaranteed delivery, in order. The server’s events are stamped from = "_authority"; a player’s events carry the player’s id.
const chat: FromPublisher = await games.publishEvent({ channel: "chat" });
chat.write(serialize({ kind: "say", text: "gg" }));
channel
string
required
Event channel name — chat, ready, rpc.use_item, etc. Each channel is its own track under game/<room>/event/<channel>.

subscribeEvents(args, cb) → GamesSubscription

Either role. Subscribes to one event channel; the callback gets the decoded sender id and the raw event bytes.
await games.subscribeEvents({ channel: "chat" }, (fromPlayerId, bytes) => {
  appendChat(fromPlayerId, deserialize(bytes));
});

FromPublisher

The handle returned by both publishInput and publishEvent. Prefixes every write with the caller’s from id.
write(payload, priority?)
void
Publish one frame. priority defaults to the channel default (100 for input, 50 for events). The from header is added for you.
close()
void
Close the underlying track.

Handles & lifecycle

GamesSubscription

Returned by every subscribe* method. Read-only ns / name plus:
close()
void
Stop receiving on this channel.

Games.close()

await games.close();   // closes the shared MoQT session + all tracks
Closes the single underlying session, which tears down every publisher and subscription on it. After close() the next publish/subscribe re-opens a fresh session.

Connection events

There is no separate event emitter — connection lifecycle is delivered through the onState / on_state callback you pass to the constructor. The underlying session auto-reconnects with capped backoff and replays every publish and subscribe on reconnect, so your channels reattach without any code on your side.
ConnectionStateMeaning
0 Connectingdialling the relay
1 Connectedsession up; channels (re)attached
2 Reconnectinglink dropped; retrying with backoff
3 Closedyou called close()
4 Failedunrecoverable (e.g. bad URL or auth)

Wire helpers (advanced)

For interop tests or callers that bypass the high-level client but stay wire-compatible, the from-header codec is exported:
import {
  encodeWithFrom,
  decodeWithFrom,
  FROM_HEADER_BYTES,   // 1
  MAX_FROM_LEN,        // 0xff
} from "@clutchcall/sdk/games";

const framed = encodeWithFrom("alice", payload);
const { fromPlayerId, payload: body } = decodeWithFrom(framed);

Other languages

The Go and Rust SDKs expose the same (room, player) model and the same three channels over the shared MoqtClient, with method names in each language’s idiomatic case. Package coordinates: github.com/clutchcall/clutchcall-sdk/go, clutchcall. Unity games use the Netcode transport drop-in rather than calling this client directly.