moonlight_core/storage/
reader.rs1use super::{scan::warn_corrupt_line, stats::StatsAccumulator};
2use crate::{run_matches_filter, ComparisonRun, ComparisonRunListItem, RunFilter, RunPage};
3use std::{
4 collections::VecDeque,
5 fs as std_fs,
6 io::{BufRead, BufReader as StdBufReader},
7 path::PathBuf,
8};
9use uuid::Uuid;
10
11#[derive(Clone)]
12pub struct JsonlStorageReader {
13 path: PathBuf,
14}
15
16impl JsonlStorageReader {
17 pub fn new(path: PathBuf) -> Self {
18 Self { path }
19 }
20
21 pub async fn stats(&self) -> anyhow::Result<crate::StatsSummary> {
22 let mut accumulator = StatsAccumulator::default();
23 self.for_each_run(|run| {
24 accumulator.record(&run);
25 true
26 })
27 .await?;
28 Ok(accumulator.finish())
29 }
30
31 pub async fn list_page(
32 &self,
33 limit: Option<usize>,
34 offset: usize,
35 ) -> anyhow::Result<Vec<ComparisonRunListItem>> {
36 Ok(self
37 .filtered_page(&RunFilter::default(), limit.unwrap_or(usize::MAX), offset)
38 .await?
39 .items)
40 }
41
42 pub async fn filtered_page(
43 &self,
44 filter: &RunFilter,
45 limit: usize,
46 offset: usize,
47 ) -> anyhow::Result<RunPage> {
48 let retained_limit = limit.saturating_add(offset);
49 let mut runs = VecDeque::new();
50 let mut total = 0;
51
52 self.for_each_run(|run| {
53 if run_matches_filter(&run, filter) {
54 total += 1;
55 if retained_limit > 0 {
56 runs.push_back(ComparisonRunListItem::from(&run));
57 if runs.len() > retained_limit {
58 runs.pop_front();
59 }
60 }
61 }
62 true
63 })
64 .await?;
65
66 let items = runs
67 .into_iter()
68 .rev()
69 .skip(offset)
70 .take(limit)
71 .collect::<Vec<_>>();
72 let next_offset = (offset + items.len() < total).then_some(offset + items.len());
73 Ok(RunPage {
74 items,
75 limit,
76 offset,
77 total,
78 next_offset,
79 })
80 }
81
82 pub async fn get(&self, id: Uuid) -> anyhow::Result<Option<ComparisonRun>> {
83 let mut found = None;
84 self.for_each_run(|run| {
85 if run.id == id {
86 found = Some(run);
87 false
88 } else {
89 true
90 }
91 })
92 .await?;
93 Ok(found)
94 }
95
96 async fn for_each_run(
97 &self,
98 mut visit: impl FnMut(ComparisonRun) -> bool,
99 ) -> anyhow::Result<()> {
100 if !self.path.try_exists()? {
101 return Ok(());
102 }
103
104 let file = std_fs::File::open(&self.path)?;
105 let lines = StdBufReader::new(file).lines();
106 for line in lines {
107 let line = line?;
108 if line.trim().is_empty() {
109 continue;
110 }
111 match serde_json::from_str::<ComparisonRun>(&line) {
112 Ok(run) => {
113 if !visit(run) {
114 break;
115 }
116 }
117 Err(error) => warn_corrupt_line(&self.path, &error),
118 }
119 }
120 Ok(())
121 }
122}