GamePeerConnection
Attributes
- auto-ready
booleanAutomatically call ready() when both channels open; fires game-start-request once peer also ready- ice-timeout
numberDefaults to2000. Milliseconds to wait for each ICE server reply from the lobby- ice-attempts
numberDefaults to3. How many times to ask the lobby for ICE servers before giving up- connect-timeout
numberDefaults to20000. Milliseconds before an incomplete handshake is abandoned; 0 waits for ever- heartbeat-interval
numberDefaults to2000. Milliseconds between heartbeats to the peer; also the latency sample rate- heartbeat-timeout
numberDefaults to10000. Milliseconds of silence from the peer before the connection is reported lost; 0 never gives up- reliable-label
.reliableLabel stringDefaults toreliable.- unreliable-label
.unreliableLabel stringDefaults tounreliable.- max-retransmits
.maxRetransmits numberDefaults to0.
Methods
- .define(tag, registry)
- .ready()
- Signal readiness to start. When both peers are ready, dispatches `game-start-request` to the shell and clears both readiness flags, so a later call starts a rematch. Equivalent to the `--peer-ready` command. Calling this before the DataChannels open is safe: readiness is sent to the peer as soon as the connection opens.
- .send(data, { reliable = true }, opts)
- Send data to the peer over a DataChannel. Data is JSON-serialised if not already a string.
- .close()
- Close the peer connection and reset to idle. Fires no `game-peer-connection-close` event; the peer is told by its own DataChannels closing.
- .reportResult(opponent, outcome)
- Report match result via the lobby signalling channel.
- .subscribe(context, callback)
Events
- no-servers
GamePeerConnectionIceEvent - game-peer-connection-open
GamePeerConnectionOpenEvent - Both DataChannels open and ready
- game-peer-connection-close
GamePeerConnectionCloseEvent - DataChannel closed, the peer left the room before the channels opened, the handshake timed out, or the peer stopped answering heartbeats
- game-peer-connection-message
GamePeerConnectionMessageEvent - Message received from peer
- game-peer-connection-ice
GamePeerConnectionIceEvent - ICE connection state changed, or "no-servers" when the lobby never returned STUN/TURN configuration
Custom States
| State | Description |
|---|---|
:state(idle) | No connection in progress |
:state(signalling) | Exchanging SDP and ICE |
:state(connecting) | ICE connecting |
:state(connected) | Both DataChannels open |
:state(disconnected) | Connection lost |
:state(ready) | Local player signalled readiness, waiting for the peer |
Manages a WebRTC RTCPeerConnection with two RTCDataChannels — one
reliable (ordered, guaranteed delivery) and one unreliable (unordered, no
retransmits). Wires automatically to a sibling <game-lobby> via the lobby
context for SDP and ICE relay.
Role determination (offerer vs answerer) is handled internally by sorting player IDs lexicographically: the lower ID becomes the offerer. Both peers reach the same conclusion independently.
Attributes
- auto-ready
booleanAutomatically call ready() when both channels open; fires game-start-request once peer also ready- ice-timeout
numberDefaults to2000. Milliseconds to wait for each ICE server reply from the lobby- ice-attempts
numberDefaults to3. How many times to ask the lobby for ICE servers before giving up- connect-timeout
numberDefaults to20000. Milliseconds before an incomplete handshake is abandoned; 0 waits for ever- heartbeat-interval
numberDefaults to2000. Milliseconds between heartbeats to the peer; also the latency sample rate- heartbeat-timeout
numberDefaults to10000. Milliseconds of silence from the peer before the connection is reported lost; 0 never gives up- reliable-label
.reliableLabel stringDefaults toreliable.- unreliable-label
.unreliableLabel stringDefaults tounreliable.- max-retransmits
.maxRetransmits numberDefaults to0.
- reliable-label
-
string— Label for the reliable DataChannel. Defaults to"reliable". Change only if you need a custom label for server-side inspection or inter-app compatibility. - unreliable-label
-
string— Label for the unreliable DataChannel. Defaults to"unreliable". - max-retransmits
-
number— Maximum retransmit attempts for the unreliable channel. Defaults to0(fire-and-forget). Increase to allow limited retries while still avoiding head-of-line blocking. - auto-ready
-
boolean— When present, calls.ready()as soon as both DataChannels open. Once the peer is ready too the element firesgame-start-request, which the shell answers by calling.start()— so the game begins with no JavaScript at all. Omit it when the players should confirm before the first game starts. - heartbeat-interval
-
number— Milliseconds between heartbeats sent to the peer over the reliable channel, and how often.latencyis sampled. Defaults to2000. - heartbeat-timeout
-
number— Milliseconds of silence from the peer (no heartbeat and no game message) before the connection is reported lost with reason"lost". Defaults to10000. Set to0to wait for ever. A killed tab or a dropped network never closes its DataChannels, and ICE can take half a minute to notice, so this is what tells a turn-based game its opponent is gone.
Instance Properties
- .peerId
-
string | null— Remote player's ID, ornullif not connected. - .localPlayerId
-
string | null— This player's ID as assigned by the signalling server (reads from the sibling<game-lobby>). - .connected
-
boolean—truewhen both DataChannels are open. - .latency
-
number | null— Last measured round-trip time in milliseconds, polled everyheartbeat-interval.nullif not connected or stats unavailable.
Instance Methods
- .send(data, options?)
-
Send data to the peer. Objects are JSON-serialised automatically; strings
are sent as-is.
Parameters:
data—any— Payload to send.options.reliable—boolean— Defaults totrue. Set tofalseto send over the unreliable channel (lower latency, no delivery guarantee).
// Reliable: game moves, state transitions match.send({ type: "move", cell: 4 }); // Unreliable: real-time position updates match.send({ x: 120, y: 340 }, { reliable: false });Calls are no-ops if the relevant channel is not open.
- .close()
-
Close the peer connection and reset to idle. Fires no
game-peer-connection-closeevent; the peer finds out through its own DataChannels closing. The match can reconnect if the lobby starts a new signalling session. Called automatically when the shell quits (shell.quit()/--quit). - .ready()
-
Signal that this player is ready to start. Readiness is sent to the peer over
the reliable channel; calling it before the channels open is safe, the signal
is sent as soon as they do. When both peers are ready the element fires
game-start-requestand clears both readiness flags, so calling.ready()again offers a rematch. Equivalent to the--peer-readycommand:<button commandfor="match" command="--peer-ready">Play again</button> - .reportResult(opponent, outcome)
-
Report a match outcome via the lobby signalling channel.
Parameters:
opponent—string— The opponent's player ID (usematch.peerId).outcome—"win" | "loss" | "draw"
match.reportResult(match.peerId, "win");
Events
- game-peer-connection-open
-
Fires once per connection, when both DataChannels are open and the connection
is ready for game data. Use
auto-readyto start the shell without writing this listener.Property Type Description peerIdstringRemote player's ID shell.addEventListener("game-peer-connection-open", (e) => { console.log("Connected to", e.peerId); shell.start(); }); - game-peer-connection-close
-
Fires when the connection is lost: the reliable DataChannel closed, the
lobby reported that the peer left the room before the channels opened (the
signalling server notices a closed tab straight away, long before ICE times
out), the handshake never completed within
connect-timeout, or nothing arrived from the peer forheartbeat-timeout. Once the channels are open the lobby is no longer consulted: the socket is handed off, and the heartbeat is what notices a vanished peer. Either way the element tears the connection down, moves to:state(disconnected)and drops any pending readiness. The unreliable channel closing on its own only fires the event.Property Type Description peerIdstringRemote player's ID reasonstring"closed"(reliable channel),"left"(peer left the room),"timeout"(handshake never completed),"lost"(peer stopped answering heartbeats) or"unreliable-closed" - game-peer-connection-message
-
Fires when a message arrives from the peer.
Property Type Description peerIdstringSender's player ID channel"reliable" | "unreliable"Which channel delivered the message dataanyJSON-parsed payload (or raw string if not valid JSON) shell.addEventListener("game-peer-connection-message", (e) => { const { data, channel, peerId } = e; if (channel === "reliable" && data.type === "move") { applyMove(data.cell); } }); - game-peer-connection-ice
-
Fires on ICE connection state changes, and once with the synthetic state
"no-servers"when the lobby never returned STUN/TURN configuration. That case still attempts the connection, but it can only succeed between peers that reach each other directly, so it is worth surfacing in the UI.Property Type Description statestringICE connection state string, or "no-servers"
All events bubble and compose.
CSS States
| State | When active |
|---|---|
:state(idle) |
No connection attempt in progress |
:state(signalling) |
Exchanging SDP and ICE candidates |
:state(connecting) |
ICE negotiation in progress |
:state(connected) |
Both DataChannels open |
:state(disconnected) |
Connection lost or peer left |
:state(ready) |
This player is ready, peer is not |
:state(ready) makes a rematch prompt purely declarative — the element sits
before the overlays in the shell, so a sibling selector reaches it:
game-peer-connection:state(ready) ~ [data-overlay] [data-rematch],
game-peer-connection:not(:state(ready)) ~ [data-overlay] [data-rematch-wait] {
display: none;
}
Shell Stats
| Stat | Type | Description |
|---|---|---|
peer-state |
string |
Current state name |
latency |
number|null |
Round-trip time in ms |
peer-id |
string |
Remote player's ID |
ice-servers |
number |
How many ICE servers the lobby gave |
game-shell.start() clears stats, so the element republishes these on the
"setup" lifecycle event. A latency readout keeps working across rounds and
rematches without any bookkeeping in your game.
Usage
<game-shell id="game" game-id="my-game" rounds="1">
<game-lobby reconnect auto-ready></game-lobby>
<game-peer-connection id="match" auto-ready></game-peer-connection>
<div when-some-scene="playing">
ping <game-signal key="latency"></game-signal>ms
</div>
</game-shell>
const shell = document.querySelector("game-shell");
const match = document.querySelector("#match");
// auto-ready starts the shell; capture the role when the connection opens
let amFirst = false;
shell.addEventListener("game-peer-connection-open", (e) => {
// Deterministic role assignment — no coordination needed
amFirst = match.localPlayerId < e.peerId;
});
// Handle incoming moves
shell.addEventListener("game-peer-connection-message", (e) => {
if (typeof e.data.cell === "number") {
applyOpponentMove(e.data.cell);
}
});
// Send a move
function makeMove(cell) {
match.send({ cell });
}
// Handle disconnect. The unreliable channel can close on its own, so only
// "closed" and "left" mean the game is over.
shell.addEventListener("game-peer-connection-close", (e) => {
if (e.reason !== "unreliable-closed") returnToLobby();
});
See the Multiplayer concept page and the Noughts & Crosses tutorial for complete integration examples.