#![expect(clippy::let_underscore_must_use, reason = "removing what is already meant to be gone")]
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use kevy_replicate::feed::{FeedPosition, fresh_generation};
fn gen_path(dir: &Path, shard: usize) -> PathBuf {
dir.join(format!("feed-{shard}.gen"))
}
fn meta_path(dir: &Path, shard: usize) -> PathBuf {
dir.join(format!("feed-{shard}.meta"))
}
pub fn write_feed_gen(dir: &Path, shard: usize, generation: u64) -> io::Result<()> {
let tmp = dir.join(format!("feed-{shard}.gen.tmp"));
{
let mut f = fs::File::create(&tmp)?;
f.write_all(generation.to_string().as_bytes())?;
f.sync_all()?;
}
fs::rename(&tmp, gen_path(dir, shard))?;
Ok(())
}
pub fn write_feed_meta(dir: &Path, shard: usize, tail: FeedPosition) -> io::Result<()> {
let tmp = dir.join(format!("feed-{shard}.meta.tmp"));
{
let mut f = fs::File::create(&tmp)?;
f.write_all(format!("{} {}", tail.generation, tail.offset).as_bytes())?;
f.sync_all()?;
}
fs::rename(&tmp, meta_path(dir, shard))?;
Ok(())
}
fn read_meta(dir: &Path, shard: usize) -> Option<(u64, u64)> {
let s = fs::read_to_string(meta_path(dir, shard)).ok()?;
let mut it = s.split_whitespace();
let g = it.next()?.parse().ok()?;
let o = it.next()?.parse().ok()?;
Some((g, o))
}
pub fn boot_position(dir: &Path, shard: usize) -> io::Result<FeedPosition> {
let highwater: Option<u64> =
fs::read_to_string(gen_path(dir, shard)).ok().and_then(|s| s.trim().parse().ok());
let marker = read_meta(dir, shard);
let _ = fs::remove_file(meta_path(dir, shard));
let at = match (highwater, marker) {
(None, _) => FeedPosition::new(fresh_generation(0), 0),
(Some(g), Some((mg, off))) if mg == g => FeedPosition::new(g, off),
(Some(g), _) => FeedPosition::new(fresh_generation(g), 0),
};
if Some(at.generation) != highwater {
write_feed_gen(dir, shard, at.generation)?;
}
Ok(at)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp() -> PathBuf {
kevy_tmpdir::unique_dir("feedmeta")
}
#[test]
fn fresh_dir_draws_a_random_gen() {
let d = tmp();
let b = boot_position(&d, 0).unwrap();
assert_ne!(b.generation, 0);
assert_eq!(b.offset, 0);
assert_eq!(fs::read_to_string(d.join("feed-0.gen")).unwrap(), b.generation.to_string());
let d2 = tmp();
let b2 = boot_position(&d2, 0).unwrap();
assert_ne!(b.generation, b2.generation);
let _ = fs::remove_dir_all(&d);
let _ = fs::remove_dir_all(&d2);
}
#[test]
fn clean_shutdown_resumes_cursor() {
let d = tmp();
let b = boot_position(&d, 0).unwrap();
write_feed_meta(&d, 0, FeedPosition::new(b.generation, 42)).unwrap();
let b2 = boot_position(&d, 0).unwrap();
assert_eq!(b2, FeedPosition::new(b.generation, 42));
let b3 = boot_position(&d, 0).unwrap();
assert_ne!(b3.generation, b.generation);
assert_ne!(b3.generation, 0);
assert_eq!(b3.offset, 0);
let _ = fs::remove_dir_all(&d);
}
#[test]
fn unclean_boot_bumps_and_persists_highwater() {
let d = tmp();
let g1 = boot_position(&d, 0).unwrap().generation;
let b = boot_position(&d, 0).unwrap();
assert_ne!(b.generation, g1);
assert_ne!(b.generation, 0);
assert_eq!(fs::read_to_string(d.join("feed-0.gen")).unwrap(), b.generation.to_string());
let _ = fs::remove_dir_all(&d);
}
#[test]
fn mismatched_marker_bumps() {
let d = tmp();
let g1 = boot_position(&d, 0).unwrap().generation;
write_feed_meta(&d, 0, FeedPosition::new(99, 7)).unwrap(); let b = boot_position(&d, 0).unwrap();
assert_ne!(b.generation, g1);
assert_eq!(b.offset, 0);
let _ = fs::remove_dir_all(&d);
}
#[test]
fn shards_are_independent() {
let d = tmp();
let g0 = boot_position(&d, 0).unwrap().generation;
write_feed_meta(&d, 0, FeedPosition::new(g0, 10)).unwrap();
let _ = boot_position(&d, 1).unwrap(); let b0 = boot_position(&d, 0).unwrap();
assert_eq!(b0.offset, 10);
let _ = fs::remove_dir_all(&d);
}
#[test]
fn a_generation_that_cannot_be_persisted_fails_the_boot() {
let d = tmp();
let missing = d.join("gone");
let err = boot_position(&missing, 0).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
let _ = fs::remove_dir_all(&d);
}
}