raft-io 0.4.0

Raft consensus and replicated-log engine for Rust. Leader election, log replication, membership changes, and snapshotting over a pluggable transport and a pluggable log store. The consensus layer above wal-db and the coordination substrate for Hive DB clustering.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
<h1 align="center">
    <img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
    <br><b>raft-io</b><br>
    <sub><sup>API REFERENCE</sup></sub>
</h1>
<div align="center">
    <sup>
        <a href="../README.md" title="Project Home"><b>HOME</b></a>
        <span>&nbsp;&nbsp;</span>
        <span>API</span>
        <span>&nbsp;&nbsp;</span>
        <a href="../CHANGELOG.md" title="Changelog"><b>CHANGELOG</b></a>
    </sup>
</div>
<br>

> Complete reference for every public item in `raft-io`, with examples.
>
> **Status: pre-1.0 (`v0.4`).** This document tracks the API surface as it lands
> across the 0.x series. The wire protocol and trait seams are frozen at `1.0`.
> Sections marked _(planned: vX.Y)_ describe a surface a later phase introduces.

## Table of Contents

- [Overview]#overview
- [Installation]#installation
- [Quick Start]#quick-start
- [The three tiers]#the-three-tiers
- [Public API]#public-api
  - [Value types]#value-types[`NodeId`]#nodeid--term--index, [`Term`]#nodeid--term--index, [`Index`]#nodeid--term--index, [`Role`]#role, [`LogEntry`]#logentry, [`HardState`]#hardstate
  - [`RaftConfig`]#raftconfig
  - [`RaftNode`]#raftnode
  - [`Event`]#event
  - [`Action`]#action
  - [Messages]#messages[`Message`]#message, [`RequestVote`]#requestvote, [`RequestVoteReply`]#requestvotereply, [`AppendEntries`]#appendentries, [`AppendEntriesReply`]#appendentriesreply
  - [`RaftLog`]#raftlog, [`MemoryLog`]#memorylog & [`WalLog`]#wallog
  - [`RaftTransport`]#rafttransport & [`MemoryTransport`]#memorytransport
  - [`Error`]#error & [`Result`]#result
- [Feature flags]#feature-flags

---

## Overview

`raft-io` implements the Raft consensus algorithm as a **deterministic,
sans-I/O state machine**. You feed a [`RaftNode`](#raftnode) [`Event`](#event)s
— logical ticks, inbound [`Message`](#message)s, client proposals — through one
method, [`step`](#step), and it returns the [`Action`](#action)s the outside
world must perform: messages to send and committed commands to apply. The node
never reads a clock, opens a socket, or touches a disk; time, networking, and
storage are injected through the [`RaftLog`](#raftlog) and
[`RaftTransport`](#rafttransport) seams. That separation is what makes the core
provable — an entire run is reproducible from a seed and a sequence of events.

At `v0.4` the implemented surface is leader election with full term and vote
safety, the complete multi-node log-replication pipeline (batched
[`AppendEntries`](#appendentries), per-follower progress with optimistic
pipelining, conflict-hint backtracking, commit on a quorum), and **durable
persistence**: [`WalLog`](#wallog), a `wal-db`-backed [`RaftLog`](#raftlog) under
the `persistence` feature whose entries and hard state survive a restart.
Snapshots and log compaction are `v0.5`.

---

## Installation

```toml
[dependencies]
raft-io = "0.4"

# Durable, crash-recoverable log (wal-db-backed `WalLog`):
raft-io = { version = "0.4", features = ["persistence"] }
```

MSRV: Rust 1.85 (edition 2024).

---

## Quick Start

```rust
use raft_io::{Action, Event, RaftConfig, RaftNode};

let mut node = RaftNode::new(RaftConfig::single(1));

while !node.is_leader() {
    let _ = node.step(Event::Tick).expect("tick never fails in memory");
}

let actions = node.step(Event::Propose(b"set x = 1".to_vec())).unwrap();
assert!(actions.iter().any(|a| matches!(a, Action::Apply { .. })));
assert_eq!(node.commit_index(), 1);
```

Runnable examples cover each path end to end:

```bash
cargo run --example single_node         # elect + propose + apply, one node
cargo run --example in_memory_cluster   # a 3-node cluster electing a leader
cargo run --example replicated_log      # propose + replicate; all nodes agree
cargo run --example partition_recovery  # minority stalls, majority commits, heal
cargo run --example persistent_node --features persistence  # log survives a restart
```

---

## The three tiers

The API is layered so the common case is trivial and the advanced case is still
reachable without ceremony.

- **Tier 1 — the lazy path.** [`RaftNode::new`]#new with a
  [`RaftConfig`]#raftconfig. No builder, no generic to name, an in-memory log
  by default. This is the whole common case.
- **Tier 2 — the configured path.** [`RaftConfig`]#raftconfig's builder
  ([`with_election_timeout`]#with_election_timeout,
  [`with_heartbeat_interval`]#with_heartbeat_interval,
  [`with_seed`]#with_seed) for tuning election and heartbeat timing.
- **Tier 3 — the power path.** The [`RaftLog`]#raftlog and
  [`RaftTransport`]#rafttransport traits, plugged in with
  [`RaftNode::with_log`]#with_log, for a durable store or a real transport.

---

## Public API

### Value types

#### `NodeId` / `Term` / `Index`

```rust
pub type NodeId = u64;
pub type Term = u64;
pub type Index = u64;
```

Three plain integer aliases that keep the hot path `Copy` and allocation-free.

- **`NodeId`** identifies a node. Opaque to the protocol; any scheme works as
  long as each node in a cluster has a distinct, stable value.
- **`Term`** is Raft's logical clock — a monotonically increasing epoch counter.
  Every message carries the sender's term; a node that sees a higher term steps
  down and adopts it. Term `0` precedes the first election.
- **`Index`** is a 1-based position in the log. The first appended entry has
  index `1`; index `0` is the sentinel "before the first entry" (term `0`).

```rust
use raft_io::{Index, NodeId, Term};

let id: NodeId = 3;
let term: Term = 5;
let index: Index = 42;
assert_eq!((id, term, index), (3, 5, 42));
```

#### `Role`

```rust
pub enum Role { Follower, Candidate, Leader }
```

The role a node currently plays. A node is always in exactly one. It starts a
`Follower`, becomes a `Candidate` when it stops hearing from a leader, and
becomes a `Leader` if it wins an election. `Copy`.

```rust
use raft_io::{RaftConfig, RaftNode, Role};

let node = RaftNode::new(RaftConfig::single(1));
assert_eq!(node.role(), Role::Follower);
```

#### `LogEntry`

```rust
pub struct LogEntry {
    pub term: Term,
    pub index: Index,
    pub command: Vec<u8>,
}
```

A single command in the replicated log. `command` is opaque bytes — the protocol
orders and replicates entries but never interprets them; the application's state
machine decodes them on apply. `term` and `index` together identify an entry
uniquely.

**Constructor**

```rust
pub fn new(term: Term, index: Index, command: Vec<u8>) -> LogEntry
```

```rust
use raft_io::LogEntry;

let entry = LogEntry::new(2, 7, b"put k v".to_vec());
assert_eq!(entry.term, 2);
assert_eq!(entry.index, 7);
assert_eq!(entry.command, b"put k v");
```

#### `HardState`

```rust
pub struct HardState {
    pub term: Term,
    pub voted_for: Option<NodeId>,
}
```

The state Raft must persist before responding to any RPC. Safety depends on
`term` and `voted_for` surviving a crash: a node that forgot it had already
voted in a term could vote twice and help elect two leaders. Stored by the
[`RaftLog`](#raftlog). Implements `Default` (term `0`, no vote).

```rust
use raft_io::HardState;

let hs = HardState { term: 4, voted_for: Some(2) };
assert_eq!(hs.term, 4);
assert_eq!(HardState::default().voted_for, None);
```

---

### `RaftConfig`

Configuration for a single node: its id, its peers, and the timing that drives
elections and heartbeats. Timing is in **logical ticks**, not wall-clock time —
the caller decides how often to tick.

**Constructors**

| Method | Signature | Description |
|---|---|---|
| <a id="new-config"></a>`new` | `fn new(id: NodeId, peers: impl IntoIterator<Item = NodeId>) -> RaftConfig` | Node `id` with the given peers (the node filters itself out of `peers`). Defaults: `10..=20` tick election timeout, `3` tick heartbeat, RNG seed = `id`. |
| `single` | `fn single(id: NodeId) -> RaftConfig` | A one-node cluster: no peers, quorum of one. |

**Builder methods** (consume and return `self`, so they chain)

| Method | Signature | Description |
|---|---|---|
| <a id="with_election_timeout"></a>`with_election_timeout` | `fn with_election_timeout(self, min: u32, max: u32) -> Self` | Randomised election-timeout bounds, in ticks. The spread breaks split votes. Normalised so `min >= 1` and `max >= min`. |
| <a id="with_heartbeat_interval"></a>`with_heartbeat_interval` | `fn with_heartbeat_interval(self, interval: u32) -> Self` | Ticks between leader heartbeats. Keep it well below the election-timeout minimum. Normalised to `>= 1`. |
| <a id="with_max_batch"></a>`with_max_batch` | `fn with_max_batch(self, max_batch: usize) -> Self` | Maximum entries carried by one `AppendEntries`. Bounds message size and per-RPC work so a far-behind follower is caught up in steady chunks. Normalised to `>= 1`. Default `64`. |
| <a id="with_seed"></a>`with_seed` | `fn with_seed(self, seed: u64) -> Self` | Seed for the deterministic election-timeout RNG. Give peers distinct seeds (the default is the node id). |

**Accessors:** `id() -> NodeId`, `peers() -> &[NodeId]`,
`election_timeout() -> (u32, u32)`, `heartbeat_interval() -> u32`,
`max_batch() -> usize`, `seed() -> u64`.

```rust
use raft_io::RaftConfig;

// Tier 1: defaults.
let cfg = RaftConfig::new(1, [2, 3]);
assert_eq!(cfg.peers(), &[2, 3]);

// Tier 2: tuned timing for a faster-ticking deployment.
let tuned = RaftConfig::new(1, [2, 3])
    .with_election_timeout(150, 300)
    .with_heartbeat_interval(30)
    .with_seed(0xABCD);
assert_eq!(tuned.election_timeout(), (150, 300));
assert_eq!(tuned.heartbeat_interval(), 30);
```

Normalisation makes degenerate input safe rather than a panic:

```rust
use raft_io::RaftConfig;

let cfg = RaftConfig::single(1).with_election_timeout(30, 10); // max < min
assert_eq!(cfg.election_timeout(), (30, 30));
```

---

### `RaftNode`

```rust
pub struct RaftNode<L: RaftLog = MemoryLog> { /* … */ }
```

A node in a Raft cluster — the deterministic consensus state machine. The
generic `L` defaults to [`MemoryLog`](#memorylog), so the common case never has
to name it.

**Constructors**

| Method | Signature | Description |
|---|---|---|
| <a id="new"></a>`new` | `fn new(config: RaftConfig) -> RaftNode<MemoryLog>` | Tier 1. Backs the node with an in-memory log. Starts as a follower in term `0`. |
| <a id="with_log"></a>`with_log` | `fn with_log(config: RaftConfig, log: L) -> RaftNode<L>` | Tier 3. Backs the node with any [`RaftLog`]#raftlog. Adopts the log's persisted [`HardState`]#hardstate on construction, so a store recovered from disk resumes in its last term and vote. |

**Accessors**

| Method | Returns | Description |
|---|---|---|
| `id` | `NodeId` | This node's id. |
| `role` | `Role` | The role the node currently plays. |
| `is_leader` | `bool` | Whether the node is the leader. |
| `term` | `Term` | The node's current term. |
| `leader` | `Option<NodeId>` | The leader the node currently recognises. |
| `commit_index` | `Index` | Highest log index known committed. |
| `last_applied` | `Index` | Highest log index applied. |
| `log` | `&L` | Shared reference to the underlying log. |

#### `step`

```rust
pub fn step(&mut self, event: Event) -> Result<Vec<Action>>
```

The only way to drive a node. Hand it one [`Event`](#event); act on every
returned [`Action`](#action), **in order** — anything the protocol depends on is
persisted before a `Send` is emitted, so honouring the order preserves Raft's
durability rule. Deterministic: the same node state and the same event always
produce the same actions.

**Parameters**

- `event` — the input: [`Event::Tick`]#event, [`Event::Message`]#event, or
  [`Event::Propose`]#event.

**Errors**

- [`Error::NotLeader`]#error — the event was a `Propose` and this node is not
  the leader; the error carries the known leader so the caller can redirect.
- [`Error::Storage`]#error — the underlying [`RaftLog`]#raftlog failed on the
  durability path. Fatal to the node.

**Example — single-node election and commit**

```rust
use raft_io::{Action, Event, RaftConfig, RaftNode};

let mut node = RaftNode::new(RaftConfig::single(1));
while !node.is_leader() {
    let _ = node.step(Event::Tick).unwrap();
}
let actions = node.step(Event::Propose(b"x".to_vec())).unwrap();
assert!(actions.iter().any(|a| matches!(a, Action::Apply { .. })));
```

**Example — a proposal to a follower is redirected**

```rust
use raft_io::{Error, Event, RaftConfig, RaftNode};

let mut node = RaftNode::new(RaftConfig::new(2, [1, 3]));
match node.step(Event::Propose(b"x".to_vec())) {
    Err(Error::NotLeader { leader }) => {
        // retry against `leader` once one is known
        let _ = leader;
    }
    _ => panic!("a fresh follower cannot accept proposals"),
}
```

**Example — driving a cluster (sketch)**

```rust
use raft_io::{Action, Event, Message, RaftConfig, RaftNode};

let mut node = RaftNode::new(RaftConfig::new(1, [2, 3]));
// On a timer, tick; on a received message, feed it in. Route every Send.
let actions = node.step(Event::Tick).unwrap();
for action in actions {
    match action {
        Action::Send { to, message } => {
            // deliver `message` to node `to` via your transport
            let _: (u64, Message) = (to, message);
        }
        Action::Apply { command, .. } => {
            // apply `command` to your state machine, in order
            let _ = command;
        }
        _ => {}
    }
}
```

---

### `Event`

```rust
pub enum Event {
    Tick,
    Message(Message),
    Propose(Vec<u8>),
}
```

The input to [`step`](#step). A node only changes state in response to an event,
and there are exactly three — Raft's three sources of progress:

- **`Tick`** — one logical clock tick. The caller picks the wall-clock interval.
- **`Message(Message)`** — a message arrived from a peer.
- **`Propose(Vec<u8>)`** — a client proposes a command. Only a leader may accept
  it; elsewhere [`step`]#step returns [`Error::NotLeader`]#error.

```rust
use raft_io::{Event, Message, RequestVote};

let _tick = Event::Tick;
let _propose = Event::Propose(b"cmd".to_vec());
let _msg = Event::Message(Message::RequestVote(RequestVote {
    term: 1, candidate: 2, last_log_index: 0, last_log_term: 0,
}));
```

---

### `Action`

```rust
#[non_exhaustive]
pub enum Action {
    Send { to: NodeId, message: Message },
    Apply { index: Index, term: Term, command: Vec<u8> },
}
```

What [`step`](#step) returns for the caller to carry out. The node decides
*what*; the caller makes it happen.

- **`Send { to, message }`** — deliver `message` to node `to` via the transport.
- **`Apply { index, term, command }`** — apply a committed command to the state
  machine. Applies are emitted in strictly increasing index order, each index
  once, so they can be applied blindly in sequence.

`#[non_exhaustive]`: a snapshot action joins it in `v0.5`, so a `match` must
include a wildcard arm.

```rust
use raft_io::{Action, Event, RaftConfig, RaftNode};

let mut node = RaftNode::new(RaftConfig::single(1));
while !node.is_leader() {
    let _ = node.step(Event::Tick).unwrap();
}
for action in node.step(Event::Propose(b"x".to_vec())).unwrap() {
    match action {
        Action::Send { to, message } => { let _ = (to, message); }
        Action::Apply { index, command, .. } => { let _ = (index, command); }
        _ => {}
    }
}
```

---

### Messages

The RPCs nodes exchange. The protocol never sends these itself — it emits
[`Action::Send`](#action) carrying a [`Message`](#message), and the caller
delivers it through a [`RaftTransport`](#rafttransport).
[`AppendEntries`](#appendentries) carries log entries in bounded batches when a
follower is behind and is an empty heartbeat when it is caught up; on rejection
the reply carries a conflict hint so the leader can backtrack a whole term at a
time.

#### `Message`

```rust
#[non_exhaustive]
pub enum Message {
    RequestVote(RequestVote),
    RequestVoteReply(RequestVoteReply),
    AppendEntries(AppendEntries),
    AppendEntriesReply(AppendEntriesReply),
}
```

Wraps the two RPCs and their replies. `#[non_exhaustive]` (`InstallSnapshot`
joins it in `v0.5`).

**Method** — `term(&self) -> Term` returns the term carried by any variant,
which the protocol checks first on every inbound message.

```rust
use raft_io::{AppendEntriesReply, Message};

let m = Message::AppendEntriesReply(AppendEntriesReply {
    term: 5, success: false, from: 2, match_index: 0,
    conflict_index: 1, conflict_term: 0,
});
assert_eq!(m.term(), 5);
```

#### `RequestVote`

```rust
pub struct RequestVote {
    pub term: Term,
    pub candidate: NodeId,
    pub last_log_index: Index,
    pub last_log_term: Term,
}
```

A candidate's request for a vote. A recipient grants it only if it has not voted
in this term and the candidate's log is at least as up to date as its own — the
election restriction that keeps a node missing committed entries off the throne.

#### `RequestVoteReply`

```rust
pub struct RequestVoteReply {
    pub term: Term,
    pub vote_granted: bool,
    pub from: NodeId,
}
```

A peer's answer. `from` names the responder so the candidate counts distinct
votes without relying on transport addressing.

#### `AppendEntries`

```rust
pub struct AppendEntries {
    pub term: Term,
    pub leader: NodeId,
    pub prev_log_index: Index,
    pub prev_log_term: Term,
    pub entries: Vec<LogEntry>,
    pub leader_commit: Index,
}
```

The leader's replicate-and-heartbeat RPC. With an empty `entries` list it is a
pure heartbeat that asserts leadership and resets the follower's election timer.
`prev_log_index` / `prev_log_term` let the follower verify its log matches the
leader's up to that point.

#### `AppendEntriesReply`

```rust
pub struct AppendEntriesReply {
    pub term: Term,
    pub success: bool,
    pub from: NodeId,
    pub match_index: Index,
    pub conflict_index: Index,
    pub conflict_term: Term,
}
```

A follower's answer. `success` is `true` when the log matched at
`prev_log_index`; `match_index` reports the highest index the follower now
agrees on, which the leader uses to track replication progress. On a rejection
the `conflict_index` / `conflict_term` pair lets the leader skip its
`next_index` for this follower back by a whole term in one round trip instead of
decrementing one entry at a time (the fast-backtracking optimisation). Both are
`0` on success.

```rust
use raft_io::{AppendEntries, Message};

// An empty heartbeat for term 4 from node 1.
let heartbeat = Message::AppendEntries(AppendEntries {
    term: 4, leader: 1, prev_log_index: 9, prev_log_term: 3,
    entries: Vec::new(), leader_commit: 7,
});
assert_eq!(heartbeat.term(), 4);
```

---

### `RaftLog`

The boundary between the protocol and where the log actually lives. The node
reads through it and writes through it, and treats a returned `Ok` from `sync`
as the durability point.

```rust
pub trait RaftLog {
    fn last_index(&self) -> Index;
    fn last_term(&self) -> Term;
    fn term_at(&self, index: Index) -> Option<Term>;
    fn entry(&self, index: Index) -> Option<LogEntry>;
    fn entries(&self, from: Index, to: Index) -> Vec<LogEntry>; // has a default impl
    fn append(&mut self, entries: &[LogEntry]) -> Result<()>;
    fn truncate(&mut self, from: Index) -> Result<()>;
    fn hard_state(&self) -> HardState;
    fn set_hard_state(&mut self, state: HardState) -> Result<()>;
    fn sync(&mut self) -> Result<()>;
}
```

| Method | Description |
|---|---|
| `last_index` | Index of the last entry, or `0` if empty. |
| `last_term` | Term of the last entry, or `0` if empty. |
| `term_at(index)` | Term at `index`; `Some(0)` for the sentinel `0`, `None` past the end. |
| `entry(index)` | The entry at `index`, or `None`. |
| `entries(from, to)` | Entries in the inclusive range `[from, to]` (the leader's replication batch). Has a default impl over `entry`; override for a bulk read. |
| `append(entries)` | Append entries; the first index must be `last_index() + 1` and the batch contiguous. |
| `truncate(from)` | Remove every entry with index `>= from` (`from >= 1`). |
| `hard_state` | The persisted [`HardState`]#hardstate. |
| `set_hard_state(state)` | Persist a new hard state. |
| `sync` | Flush preceding writes to durable storage. |

**Durability contract.** A backend may buffer writes, but once `sync` returns
`Ok`, every preceding `append`, `truncate`, and `set_hard_state` must be durable.
The node always calls `sync` before emitting a message that depends on that
state, honouring Raft's "persist before you respond" rule. Implementors map
their own errors into [`Error::Storage`](#error) via
[`Error::storage`](#error-helpers), so the trait's error type stays the crate's
own — no associated error type for callers to name.

### `MemoryLog`

The default, non-durable [`RaftLog`](#raftlog), backed by a `Vec`. Used by
[`RaftNode::new`](#new). For tests, examples, and the single-node path — not
production.

**Methods:** `new()`, `len() -> usize`, `is_empty() -> bool`, plus the full
[`RaftLog`](#raftlog) trait.

```rust
use raft_io::{HardState, LogEntry, MemoryLog, RaftLog};

let mut log = MemoryLog::new();
log.append(&[LogEntry::new(1, 1, b"a".to_vec())]).unwrap();
log.set_hard_state(HardState { term: 1, voted_for: Some(1) }).unwrap();
log.sync().unwrap();

assert_eq!(log.last_index(), 1);
assert_eq!(log.term_at(1), Some(1));
assert_eq!(log.term_at(0), Some(0)); // sentinel
assert_eq!(log.hard_state().voted_for, Some(1));
```

A non-contiguous append is rejected rather than allowed to corrupt the log:

```rust
use raft_io::{Error, LogEntry, MemoryLog, RaftLog};

let mut log = MemoryLog::new();
let err = log.append(&[LogEntry::new(1, 2, vec![])]).unwrap_err(); // expected index 1
assert!(matches!(err, Error::Storage { .. }));
```

### `WalLog`

_Requires the `persistence` feature._

A durable [`RaftLog`](#raftlog) backed by `wal-db`, whose entries and hard state
(term and vote) survive a process restart. This is what makes a node
crash-recoverable: Raft's safety depends on `current_term`, `voted_for`, and the
log being durable before the node acts on them.

It is log-structured. Every mutation — an appended entry, a hard-state update, a
truncation — is encoded as a record and appended to a `wal-db` write-ahead log
(which frames and checksums each record); an in-memory index mirrors the current
state for fast reads. [`open`](#wallog) replays the records to rebuild that index
exactly. Reads are served from memory; writes become durable when
[`sync`](#raftlog) returns `Ok` — and the node always `sync`s before it replies,
honouring the "persist before you respond" rule.

**Constructor**

| Method | Signature | Description |
|---|---|---|
| `open` | `fn open(path: impl AsRef<Path>) -> Result<WalLog>` | Open (creating if absent) and recover the log at `path`. Returns [`Error::Storage`]#error if the file cannot be opened or a record fails to decode. |

Plus the full [`RaftLog`](#raftlog) trait.

```rust,no_run
use raft_io::{LogEntry, RaftConfig, RaftLog, RaftNode, WalLog};

// Open a durable log and hand it to a node.
let log = WalLog::open("node-1.wal")?;
let mut node = RaftNode::with_log(RaftConfig::single(1), log);
# let _ = &mut node;

// After a restart, reopening the same path recovers the entries and the
// persisted term/vote.
let recovered = WalLog::open("node-1.wal")?;
assert_eq!(recovered.last_index(), node.log().last_index());
# Ok::<(), raft_io::Error>(())
```

Truncated entries remain physically in the WAL until log compaction (snapshots,
`v0.5`); replay still reconstructs the correct logical state. The byte-record
API of `wal-db` is used directly — `raft-io` frames its own records and does not
enable `wal-db`'s `pack-io` feature.

---

### `RaftTransport`

Delivers protocol messages to peers. A driver loop takes each
[`Action::Send`](#action) a node emits and calls `send`. How delivery happens —
an in-process queue, a channel, a socket — is the implementor's concern; the
protocol only needs a handed-off message to eventually reach the target's
[`step`](#step) (Raft tolerates loss, reordering, and duplication).

```rust
pub trait RaftTransport {
    fn send(&mut self, to: NodeId, message: Message) -> Result<()>;
}
```

### `MemoryTransport`

An in-memory [`RaftTransport`](#rafttransport) that records outgoing messages
instead of delivering them, so a test harness can route them by hand and control
ordering, loss, and partitions precisely.

**Methods:** `new()`, `take() -> Vec<(NodeId, Message)>` (drain),
`pending() -> usize`.

```rust
use raft_io::{AppendEntries, MemoryTransport, Message, RaftTransport};

let mut tx = MemoryTransport::new();
tx.send(2, Message::AppendEntries(AppendEntries {
    term: 1, leader: 1, prev_log_index: 0, prev_log_term: 0,
    entries: Vec::new(), leader_commit: 0,
})).unwrap();

let pending = tx.take();
assert_eq!(pending[0].0, 2);   // destination node
assert!(tx.take().is_empty()); // draining leaves it empty
```

---

### `Error`

```rust
#[non_exhaustive]
pub enum Error {
    NotLeader { leader: Option<NodeId> },
    Storage { context: &'static str, detail: String },
}
```

Everything that can go wrong while driving a node. Built on `error-forge`: it
implements `error_forge::ForgeError`, exposing stable `kind` / `caption` and
severity (`is_retryable` / `is_fatal`) metadata, and is an ordinary
`std::error::Error`. `#[non_exhaustive]` — match with a wildcard arm.

| Variant | Meaning | Severity |
|---|---|---|
| `NotLeader { leader }` | A proposal reached a non-leader. `leader` is the best-known leader for the caller to redirect to (`None` during an election). | Retryable, not fatal. |
| `Storage { context, detail }` | A [`RaftLog`]#raftlog backend operation failed. `context` names the operation; `detail` is the backend's message. | Fatal, not retryable. |

<a id="error-helpers"></a>**Helper** — `Error::storage(context, source)` builds a
`Storage` error from any `Display` source, so backends map their errors without
naming the fields.

```rust
use raft_io::Error;

let err = Error::NotLeader { leader: Some(3) };
assert_eq!(err.to_string(), "not the leader; current leader is node 3");

let io = std::io::Error::new(std::io::ErrorKind::Other, "disk full");
assert!(Error::storage("append entries", io).to_string().contains("disk full"));
```

### `Result`

```rust
pub type Result<T, E = Error> = core::result::Result<T, E>;
```

The crate's result alias, defaulting its error to [`Error`](#error), so most
signatures read `Result<T>`.

---

## Feature flags

| Feature | Default | Description |
|---------|---------|-------------|
| `persistence` | no | Adds [`WalLog`]#wallog, a durable `wal-db`-backed [`RaftLog`]#raftlog. The in-memory path is unaffected when off. |

One more flag is reserved for a later phase and is not yet present on the crate,
because an optional dependency without a code path that uses it would be dead
weight: **`framing`** (typed RPC/message framing via `pack-io`) lands in `v0.5`.
It will be purely additive.

---

<sub>Copyright &copy; 2026 <strong>James Gober</strong>. All rights reserved.</sub>