gameNode
Constructs a RaftNode over seam for a session whose full voter roster is already known to every peer (e.g. from matchmaking). Every peer builds the identical ClusterConfig.ofVoters and Raft's own election picks the leader — symmetric, no pre-Raft coordination step required.
This is the roster-given bootstrap path: call it when every participating peer's identity is known before the session starts. For the appoint-the-host path (dynamic join without a fixed roster), see gameHost/gameJoin.
Internal multiplexing. gameNode wraps seam in a MuxSeam so Raft traffic (channel tag 1) and the application-envelope NamedMux (channel tag 3) share the one underlying Seam. This costs Raft one extra tag byte per frame versus an unmuxed seam — the accepted price of a uniform GameSession return type and ride-along app channels across all three bootstrap paths. Drive consensus through GameSession.node; ride extra traffic over GameSession.appChannel.
Do not collect seam.incoming after calling this. Once the returned GameSession is running, the internal MuxSeam is the sole consumer of seam.incoming (ADR-034 single-collection). A second collector races the Raft engine and drops messages, causing silent liveness failures.
Parameters
The Seam connecting this peer to the rest of the cluster. This peer's identity (Seam.selfId) must appear in voterIds.
The full set of voter NodeIds for the cluster. Every peer must pass the same set; Raft's election then picks one leader symmetrically.
Durable Raft state (term, vote, log). Defaults to InMemoryRaftStorage (non-durable, suitable for short-lived sessions or tests). Inject a persistent implementation for crash-recovery.
Timing and behaviour parameters. Production callers use the default RaftConfig (real-clock, expectVirtualTime = false). Tests pass RaftConfig(expectVirtualTime = true) — this is the only supported path to virtual-time execution; gameNode deliberately does not expose expectVirtualTime as its own parameter (D4).
How this peer obtains its Raft §8 dedup id. ClientIdentity.Auto (default) mints a per-incarnation auto id (at-least-once forwarding, no cross-crash dedup). A durable peer passes ClientIdentity.Durable with a stable id it persists itself and replays the same requestId on TurnSequencer.propose after a restart. See us.tractat.kuilt.raft.ClientSessionTable.
Throws
Samples
runTest(StandardTestDispatcher(), timeout = 5.seconds) {
val loom = InMemoryLoom()
val seam1 = loom.host(Pattern("my-game"))
val seam2 = loom.join(InMemoryTag("player-2"))
val id1 = NodeId(seam1.selfId.value)
val id2 = NodeId(seam2.selfId.value)
val voterIds = setOf(id1, id2)
// Both peers call gameNode with the same voter set. Raft elects one leader.
val session1 = backgroundScope.gameNode(seam1, voterIds, raftConfig = RaftConfig(expectVirtualTime = true))
val session2 = backgroundScope.gameNode(seam2, voterIds, raftConfig = RaftConfig(expectVirtualTime = true))
// Both sessions are live. Drive the game through TurnSequencer over either node:
// propose() is forwarded to the leader transparently by Raft.
// For a concrete propose + committed example see sampleGameHostJoin.
TurnSequencer(session1.node, Int.serializer())
TurnSequencer(session2.node, Int.serializer())
// Ride named application channels (chat, cursors, …) over the same fabric.
val chatIncoming = async { session2.appChannel("chat").incoming.first() }
session1.appChannel("chat").broadcast(byteArrayOf(0x68, 0x69)) // "hi"
assertEquals(2, chatIncoming.await().payloadSize)
session1.close()
session2.close()
}