TurnSequencer

class TurnSequencer<A>(node: RaftNode, serializer: KSerializer<A>, format: BinaryFormat = DEFAULT_FORMAT)

A game-developer-friendly facade over RaftNode.

Hides most Raft machinery — terms, log entries, election no-ops — and exposes only the concepts a turn-based game needs:

  • propose — submit a typed action; suspends until a quorum commits it and returns the IndexedAction with the assigned position in the log.

  • events — a Flow of TurnEvents in order, on every node (leader and followers alike): committed actions decoded from Raft's opaque bytes (TurnEvent.Committed) and, when log compaction is enabled, snapshot installs (TurnEvent.Reset).

The one Raft concept that necessarily surfaces is the snapshot install, as TurnEvent.Reset — a consumer that enables compaction must reset its state machine, so the event cannot be hidden. Terms, log indices as raw entries, and the §5.4.2 no-op stay invisible.

val sequencer = TurnSequencer(node, Move.serializer())
val table = ClientSessionTable()

// On every node — drive the state machine off the turn-event stream:
scope.launch {
sequencer.events.collect { event ->
when (event) {
is TurnEvent.Committed -> {
val (index, move, key) = event.indexed
if (table.shouldApply(key)) applyMove(index, move) // exactly-once
}
is TurnEvent.Reset -> resetStateMachine(event.snapshot)
}
}
}

// On any node — propose the local player's move (forwarded to the leader if needed):
val indexed = sequencer.propose(Move(player = 0, card = 3))
println("Move committed at index ${indexed.index}")

// A durable client replays the same requestId after a crash to get cross-crash exactly-once:
sequencer.propose(Move(player = 0, card = 3), requestId = nextSerial)

Parameters

node

The backing RaftNode. Lifetime is owned by the caller; this facade does not close node when done.

serializer

The KSerializer used to encode actions to bytes for Raft replication and to decode committed bytes back to A.

format

The BinaryFormat used to encode and decode actions. This becomes the single source of truth for the wire encoding of every log entry, so any replay layer (e.g. snapshot / log scan) must use the same format instance to produce byte-identical payloads. Defaults to a shared CBOR instance.

Note: A is invariant because it appears in both input position (propose(action: A)) and output position (events). Only IndexedAction and TurnEvent are covariant (out A) since they are pure output carriers.

Constructors

Link copied to clipboard
constructor(node: RaftNode, serializer: KSerializer<A>, format: BinaryFormat = DEFAULT_FORMAT)

Properties

Link copied to clipboard
val events: Flow<TurnEvent<A>>

A hot Flow of TurnEvents in index order, emitted on every node in the cluster.

Functions

Link copied to clipboard
suspend fun propose(action: A): IndexedAction<A>

Proposes action for replication and suspends until a quorum commits it, drawing the next per-node monotonic serial as the dedup requestId.

suspend fun propose(action: A, requestId: Long): IndexedAction<A>

Proposes action with a caller-pinned requestId (Raft §8 client serial) under the backing RaftNode's clientId, then suspends until a quorum commits it (same semantics as propose).