# The Transport contract (dev-contract §5)

What `room: { id, appId }` implements for you (Trystero over the facade's vetted relay
list), and what a custom `transport` object must provide. This interface is versioned with
the dev contract: a transport written against it keeps working across EM versions.

## Interface

| Member | Contract |
|---|---|
| `localId` | Stable string identity of this peer for the session's lifetime. Becomes the participant id. |
| `connect()` | Join the room/network. Idempotent. Peer discovery and message flow start after this. |
| `disconnect()` | Leave. After it, `send`/`broadcast` are no-ops (never throws for late calls). |
| `send(peerId, message, opts?)` | Deliver `message` to one peer. `opts.reliable: true` messages MUST arrive, in order (recovery, checkpoint asserts, and state transfers depend on it). Best-effort otherwise. Unknown/departed peer: silent no-op (the peer list is eventually consistent). |
| `broadcast(message, opts?)` | Deliver to every current peer. Same reliability rule. |
| `onMessage(cb) → off` | `cb(fromPeerId, message)`. Messages are opaque structured-cloneable values; deliver them unmodified. Returns an unsubscribe. |
| `onPeerJoin(cb) → off` | `cb(peerId)` when a peer becomes reachable. |
| `onPeerLeave(cb) → off` | `cb(peerId)` when a peer departs or times out. |
| `getPeers()` | Array of currently-reachable peer ids (excluding self). Eventually consistent is fine. |
| `attachClockSync(cb) → off` | Transport-internal channel for clock-sync ping/pong: `cb(fromPeerId, payload)`; the transport must carry these like ordinary traffic (a partition that cuts game traffic must cut clock sync too — no sidecar) and must NOT surface them through `onMessage`. |

## Semantics that matter

- **Reliability is load-bearing.** Everything marked `reliable` is protocol-critical:
  losing one silently can wedge recovery. If your medium cannot guarantee delivery,
  implement retry/ack underneath before claiming the flag.
- **Identity is per-session.** A refresh/rejoin may present a new `localId`; EM treats it
  as a returning participant through its own machinery — do not try to be clever about
  reusing ids.
- **No message inspection.** EM's envelopes are internal; carry them byte-faithfully.
- **Liveness is yours.** `onPeerLeave`/`getPeers` reflect the transport's own heartbeat or
  signalling. EM layers engine-level attendance ON TOP; it does not replace transport
  liveness (see `stalePeerIds` in the status contract).

## The room path

`room: { id, appId }` constructs `TrysteroTransport` with the facade's `VETTED_RELAY_URLS`
(tracker-outage hardening: the compiled trystero defaults are mostly dead) and
`relayRedundancy: 4`. The `appId` is always explicit — it namespaces your rooms globally,
so collisions across apps cannot pair. In Node/tests, inject
`advanced.trysteroBinding: { joinRoom, selfId }` to skip the remote trystero module.
