1use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::persistence::{self, StoreError};
9
10use super::job::Job;
11
12const STORE_FILE: &str = "jobs.json";
13const SCHEMA_VERSION: u32 = 1;
14const DEFAULT_LIMIT: usize = 50;
15
16#[derive(Serialize, Deserialize)]
17struct Envelope {
18 schema_version: u32,
19 jobs: Vec<Job>,
20}
21
22pub struct JobHistoryStore {
25 directory: PathBuf,
26 limit: usize,
27 jobs: Vec<Job>,
28 loaded: bool,
29}
30
31impl JobHistoryStore {
32 pub fn new(directory: &Path, limit: usize) -> Self {
34 Self {
35 directory: directory.to_path_buf(),
36 limit: limit.max(1),
37 jobs: Vec::new(),
38 loaded: false,
39 }
40 }
41
42 pub fn with_default_limit(directory: &Path) -> Self {
44 Self::new(directory, DEFAULT_LIMIT)
45 }
46
47 pub fn limit(&self) -> usize {
49 self.limit
50 }
51
52 pub fn set_limit(&mut self, limit: usize) {
54 self.limit = limit.max(1);
55 }
56
57 pub fn record(&mut self, job: Job) -> Result<(), StoreError> {
60 self.load_if_needed();
61 self.jobs.retain(|existing| existing.id != job.id);
62 self.jobs.push(job);
63 self.jobs
64 .sort_by(|a, b| (b.submitted_at, &b.id).cmp(&(a.submitted_at, &a.id)));
65 self.jobs.truncate(self.limit);
66 self.save()
67 }
68
69 pub fn list(&mut self) -> &[Job] {
71 self.load_if_needed();
72 &self.jobs
73 }
74
75 pub fn get(&mut self, id: &str) -> Option<Job> {
77 self.load_if_needed();
78 self.jobs.iter().find(|job| job.id == id).cloned()
79 }
80
81 fn store_file(&self) -> PathBuf {
82 self.directory.join(STORE_FILE)
83 }
84
85 fn load_if_needed(&mut self) {
86 if self.loaded {
87 return;
88 }
89 self.loaded = true;
90 if let Ok(Some(envelope)) = persistence::read_json::<Envelope>(&self.store_file()) {
93 self.jobs = envelope.jobs;
94 }
95 }
96
97 fn save(&self) -> Result<(), StoreError> {
98 let envelope = Envelope {
99 schema_version: SCHEMA_VERSION,
100 jobs: self.jobs.clone(),
101 };
102 persistence::write_json_atomic(&self.store_file(), &envelope)
103 }
104}