roomId

abstract val roomId: StateFlow<RoomId?>

Which room this is — the identity every member of one session agrees on, or null while this peer does not know it yet.

One value per room instance, not per device: two rooms the same peer hosts in a row have two different ids, and so do the rooms either side of an app restart. That is what makes it usable as a per-session key — a durable (roomId, deviceId) → seat record, a per-game log scope, a lobby identifier — rather than merely as a label for the host.

Both roles read the same value, but they learn it at different moments:

  • Host — non-null from construction. RoomFactory.host mints it (or uses the one the caller supplied) before the room exists, so it is readable immediately.

  • Joinernull until admitted. The host sends its id in the admit Welcome, so this stays null through the handshake and flips once, to the host's value, when the joiner is admitted. Collect it (or read it after the roster becomes non-empty) rather than sampling it right after RoomFactory.join returns.

This is the id a ResumeToken is validated against, within one live host: a joiner's token carries this value and the host refuses a token naming any other room.

That does not extend across a host restart, and reusing an id will not make it. A restarted host has no memory of the peer — an empty roster and an empty reconnect-window registry — so a resume it accepts on identity grounds still cannot complete. Cold-start rejoin is tracked separately by #1593 and needs more than a stable id.

Deliberately has no interface default. A default would let an implementation — a test double above all — silently answer "this room has no identity" for a room that plainly has one, and a test written against that answer would pass while asserting nothing.