1#![forbid(unsafe_code)]
2
3use std::cmp::Ordering;
4
5pub mod metadata;
6pub mod reuse;
7
8#[must_use]
10pub fn crate_id() -> &'static str {
11 "wombatkv-core"
12}
13
14pub type WriteId = [u8; 16];
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct FactRef {
20 pub epoch_ms: u64,
22 pub write_id: WriteId,
24}
25
26impl FactRef {
27 #[must_use]
29 pub const fn ordering_key(&self) -> (u64, WriteId) {
30 (self.epoch_ms, self.write_id)
31 }
32}
33
34#[must_use]
36pub fn pick_winner(facts: &[FactRef]) -> Option<&FactRef> {
37 facts.iter().min_by(|lhs, rhs| compare_facts(lhs, rhs))
38}
39
40#[must_use]
42pub fn compare_facts(lhs: &FactRef, rhs: &FactRef) -> Ordering {
43 lhs.ordering_key().cmp(&rhs.ordering_key())
44}
45
46#[must_use]
48pub fn wal_chunk_key(
49 tenant: &str,
50 fingerprint: &str,
51 epoch_ms: u64,
52 node_id: &str,
53 seq: u64,
54) -> String {
55 format!("wal/{tenant}/{fingerprint}/{epoch_ms}/chunk-{node_id}-{seq}.bin")
56}
57
58#[must_use]
60pub fn contiguous_resident_prefix(residency: &[bool]) -> usize {
61 residency.iter().take_while(|is_resident| **is_resident).count()
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum HashFunction {
67 Blake3,
68 XxHash3,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum WireFormat {
74 MsgPack,
75 FlatBuffers,
76 Capnp,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct M0Defaults {
82 pub block_size_tokens: u16,
83 pub hash_function: HashFunction,
84 pub wal_chunk_target_mb: u16,
85 pub wire_format: WireFormat,
86}
87
88impl Default for M0Defaults {
89 fn default() -> Self {
90 Self {
91 block_size_tokens: 128,
92 hash_function: HashFunction::Blake3,
93 wal_chunk_target_mb: 32,
94 wire_format: WireFormat::MsgPack,
95 }
96 }
97}
98
99impl M0Defaults {
100 pub fn validate(self) -> Result<(), &'static str> {
102 const VALID_BLOCK_SIZES: [u16; 3] = [64, 128, 256];
103 const VALID_WAL_CHUNK_MB: [u16; 3] = [8, 32, 64];
104
105 if !VALID_BLOCK_SIZES.contains(&self.block_size_tokens) {
106 return Err("block_size_tokens must be one of 64, 128, 256");
107 }
108
109 if !VALID_WAL_CHUNK_MB.contains(&self.wal_chunk_target_mb) {
110 return Err("wal_chunk_target_mb must be one of 8, 32, 64");
111 }
112
113 Ok(())
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::{compare_facts, contiguous_resident_prefix, pick_winner, wal_chunk_key, FactRef};
120
121 #[test]
122 fn crate_id_is_stable() {
123 assert_eq!(super::crate_id(), "wombatkv-core");
124 }
125
126 #[test]
127 fn epoch_and_write_id_define_total_order() {
128 let earlier = FactRef { epoch_ms: 1_700_000_000_001, write_id: [2; 16] };
129 let later = FactRef { epoch_ms: 1_700_000_000_002, write_id: [1; 16] };
130 let same_epoch_lower_write = FactRef { epoch_ms: 1_700_000_000_001, write_id: [1; 16] };
131
132 assert!(compare_facts(&earlier, &later).is_lt());
133 assert!(compare_facts(&same_epoch_lower_write, &earlier).is_lt());
134 }
135
136 #[test]
137 fn winner_is_stable_independent_of_arrival_order() {
138 let winner = FactRef { epoch_ms: 1_700_000_000_100, write_id: [1; 16] };
139 let loser = FactRef { epoch_ms: 1_700_000_000_100, write_id: [9; 16] };
140
141 let first_order = vec![loser.clone(), winner.clone()];
142 let second_order = vec![winner.clone(), loser];
143
144 assert_eq!(pick_winner(&first_order), Some(&winner));
145 assert_eq!(pick_winner(&second_order), Some(&winner));
146 }
147
148 #[test]
149 fn wal_keys_are_unique_across_nodes_for_same_epoch_and_seq() {
150 let key_a = wal_chunk_key("t1", "f1", 1000, "node-a", 7);
151 let key_b = wal_chunk_key("t1", "f1", 1000, "node-b", 7);
152 assert_ne!(key_a, key_b);
153 }
154
155 #[test]
156 fn contiguous_prefix_stops_at_first_missing_block() {
157 let residency = [true, true, false, true];
158 assert_eq!(contiguous_resident_prefix(&residency), 2);
159 }
160
161 #[test]
162 fn m0_defaults_are_stable_and_valid() {
163 let defaults = super::M0Defaults::default();
164 assert_eq!(defaults.block_size_tokens, 128);
165 assert_eq!(defaults.hash_function, super::HashFunction::Blake3);
166 assert_eq!(defaults.wal_chunk_target_mb, 32);
167 assert_eq!(defaults.wire_format, super::WireFormat::MsgPack);
168 assert_eq!(defaults.validate(), Ok(()));
169 }
170
171 #[test]
172 fn invalid_block_size_is_rejected() {
173 let invalid = super::M0Defaults { block_size_tokens: 96, ..super::M0Defaults::default() };
174 assert_eq!(invalid.validate(), Err("block_size_tokens must be one of 64, 128, 256"));
175 }
176
177 #[test]
178 fn invalid_wal_chunk_size_is_rejected() {
179 let invalid = super::M0Defaults { wal_chunk_target_mb: 16, ..super::M0Defaults::default() };
180 assert_eq!(invalid.validate(), Err("wal_chunk_target_mb must be one of 8, 32, 64"));
181 }
182}