gameSpectate

suspend fun CoroutineScope.gameSpectate(seam: Seam, storage: RaftStorage = InMemoryRaftStorage(), raftConfig: RaftConfig = RaftConfig(), spectateAdmissionTimeout: Duration = DEFAULT_SPECTATE_ADMISSION_TIMEOUT, identity: ClientIdentity = ClientIdentity.Auto): GameSession

Join a game session over seam as a permanent, non-voting spectator learner.

A spectator receives the full committed log (log replication + chunked InstallSnapshot) and follows the game live, but never votes and never counts toward quorum. The session's voter quorum is unaffected by how many spectators are present — two voters can still commit with a spectator watching.

Host opt-in required. The host must call gameHost with allowSpectators = true and maxSpectators >= 1. If spectators are disabled (the default) or the cap is already reached, gameSpectate throws SpectatorsClosedException immediately — never a silent hang.

Permanent role — no promotion. A spectator's GameSession.node role is permanently RaftRole.Learner. There is no promotion-to-voter path in this entry point; that is a separate concern (see issue #594).

Internal multiplexing. gameSpectate wraps seam in a MuxSeam and routes Raft traffic on channel tag 1, lobby presence on channel tag 2, and the application-envelope NamedMux on channel tag 3 — matching gameHost's channels. The caller passes a plain Seam; muxing is internal. Ride extra application traffic over GameSession.appChannel.

Do not collect seam.incoming after calling this (ADR-034 single-collection).

Parameters

storage

Durable Raft state. Defaults to InMemoryRaftStorage.

raftConfig

Timing and behaviour parameters. Tests pass RaftConfig(expectVirtualTime = true) (D4).

spectateAdmissionTimeout

Upper bound on waiting for the host to either admit this spectator or signal spectators-closed. If this bound expires before either signal arrives, gameSpectate throws SpectateTimeoutException. The default is sized to clear a typical WAN round-trip; lower it in tests where you want the backstop to fire quickly.

identity

How this spectator obtains its Raft §8 dedup id. ClientIdentity.Auto (default) mints a per-incarnation auto id. A spectator never proposes, so this is rarely needed; accepted for symmetry with the other bootstrap paths. See us.tractat.kuilt.raft.ClientSessionTable.

Throws

if the host has spectators disabled or the cap is full.

if neither admission nor a spectators-closed signal arrives within spectateAdmissionTimeout.