Skip to main content

gity_ipc/
lib.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, path::PathBuf, time::SystemTime};
4use thiserror::Error;
5
6mod bincode_serde;
7mod validated_path;
8
9pub use bincode_serde::{bounded_bincode, validate_message_size, MessageSizeError};
10pub use validated_path::{PathValidationError, ValidatedPath};
11
12/// All commands the CLI/tray can send to the daemon process.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum DaemonCommand {
15    RegisterRepo {
16        repo_path: ValidatedPath,
17    },
18    UnregisterRepo {
19        repo_path: ValidatedPath,
20    },
21    ListRepos,
22    Status {
23        repo_path: ValidatedPath,
24        known_generation: Option<u64>,
25    },
26    QueueJob {
27        repo_path: ValidatedPath,
28        job: JobKind,
29    },
30    HealthCheck,
31    /// Request detailed health diagnostics for a specific repository.
32    RepoHealth {
33        repo_path: ValidatedPath,
34    },
35    Metrics,
36    FsMonitorSnapshot {
37        repo_path: ValidatedPath,
38        last_seen_generation: Option<u64>,
39    },
40    FetchLogs {
41        repo_path: ValidatedPath,
42        limit: usize,
43    },
44    /// Request graceful daemon shutdown.
45    Shutdown,
46}
47
48/// Every response the daemon can emit. Real IPC will eventually serialize this
49/// across async-nng sockets.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub enum DaemonResponse {
52    Ack(Ack),
53    RepoList(Vec<RepoSummary>),
54    RepoStatus(RepoStatusDetail),
55    RepoStatusUnchanged { repo_path: PathBuf, generation: u64 },
56    Health(DaemonHealth),
57    RepoHealth(RepoHealthDetail),
58    Metrics(DaemonMetrics),
59    FsMonitorSnapshot(FsMonitorSnapshot),
60    Logs(Vec<LogEntry>),
61    Error(String),
62}
63
64/// Lightweight acknowledgement wrapper used by multiple commands.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Ack {
67    pub message: String,
68}
69
70impl Ack {
71    pub fn new(message: impl Into<String>) -> Self {
72        Self {
73            message: message.into(),
74        }
75    }
76}
77
78/// Snapshot of daemon-level health information.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct DaemonHealth {
81    pub repo_count: usize,
82    pub pending_jobs: usize,
83    pub uptime_seconds: u64,
84    pub repo_generations: Vec<RepoGeneration>,
85}
86
87/// Detailed health diagnostics for a specific repository.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct RepoHealthDetail {
90    pub repo_path: PathBuf,
91    pub generation: u64,
92    pub pending_jobs: usize,
93    pub watcher_active: bool,
94    pub last_event: Option<SystemTime>,
95    pub dirty_path_count: usize,
96    pub sled_ok: bool,
97    pub needs_reconciliation: bool,
98    pub throttling_active: bool,
99    pub next_scheduled_job: Option<String>,
100}
101
102/// Generation token for a registered repository.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct RepoGeneration {
105    pub repo_path: PathBuf,
106    pub generation: u64,
107}
108
109/// Snapshot of daemon metrics such as job counters and resource usage.
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct DaemonMetrics {
112    pub jobs: HashMap<JobKind, JobMetrics>,
113    pub global: GlobalMetrics,
114    pub repos: Vec<RepoMetrics>,
115}
116
117impl DaemonMetrics {
118    pub fn new() -> Self {
119        Self {
120            jobs: HashMap::new(),
121            global: GlobalMetrics::default(),
122            repos: Vec::new(),
123        }
124    }
125}
126
127impl Default for DaemonMetrics {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133/// Aggregate daemon-level statistics.
134#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
135pub struct GlobalMetrics {
136    pub pending_jobs: usize,
137    pub uptime_seconds: u64,
138    pub cpu_percent: f32,
139    pub rss_bytes: u64,
140}
141
142/// Lightweight view into per-repository queue depth.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct RepoMetrics {
145    pub repo_path: PathBuf,
146    pub pending_jobs: usize,
147}
148
149/// Metadata describing a repository within the daemon.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct RepoSummary {
152    pub repo_path: PathBuf,
153    pub status: RepoStatus,
154    pub pending_jobs: usize,
155    pub last_event: Option<SystemTime>,
156    pub generation: u64,
157}
158
159impl RepoSummary {
160    pub fn new(repo_path: PathBuf) -> Self {
161        Self {
162            repo_path,
163            status: RepoStatus::Unknown,
164            pending_jobs: 0,
165            last_event: None,
166            generation: 0,
167        }
168    }
169}
170
171/// Details about a repository used by `gity status`.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct RepoStatusDetail {
174    pub repo_path: PathBuf,
175    pub dirty_paths: Vec<PathBuf>,
176    pub generation: u64,
177}
178
179/// Snapshot of paths that changed since the previous fsmonitor token.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct FsMonitorSnapshot {
182    pub repo_path: PathBuf,
183    pub dirty_paths: Vec<PathBuf>,
184    pub generation: u64,
185}
186
187/// A coarse view of repository health. The daemon tightens this as the
188/// implementation matures.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub enum RepoStatus {
191    Idle,
192    Busy,
193    Unknown,
194}
195
196impl RepoStatus {
197    pub fn as_str(&self) -> &'static str {
198        match self {
199            RepoStatus::Idle => "idle",
200            RepoStatus::Busy => "busy",
201            RepoStatus::Unknown => "unknown",
202        }
203    }
204}
205
206/// Job types queued within the daemon scheduler.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
208pub enum JobKind {
209    Prefetch,
210    Maintenance,
211}
212
213impl JobKind {
214    pub const ALL: [JobKind; 2] = [JobKind::Prefetch, JobKind::Maintenance];
215
216    pub fn as_str(self) -> &'static str {
217        match self {
218            JobKind::Prefetch => "prefetch",
219            JobKind::Maintenance => "maintenance",
220        }
221    }
222}
223
224/// Counters describing how jobs progressed through the scheduler.
225#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct JobMetrics {
227    pub spawned: u64,
228    pub completed: u64,
229    pub failed: u64,
230}
231
232/// Streaming notifications emitted by the daemon.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub enum DaemonNotification {
235    WatchEvent(WatchEventNotification),
236    JobEvent(JobEventNotification),
237    Log(LogEntry),
238    RepoStatus(RepoStatusDetail),
239}
240
241/// A filesystem event observed for a registered repository.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct WatchEventNotification {
244    pub repo_path: PathBuf,
245    pub path: PathBuf,
246    pub kind: WatchEventKind,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250pub enum WatchEventKind {
251    Created,
252    Modified,
253    Deleted,
254}
255
256/// Lifecycle event for a background job.
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct JobEventNotification {
259    pub repo_path: PathBuf,
260    pub job: JobKind,
261    pub kind: JobEventKind,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub enum JobEventKind {
266    Queued,
267    Started,
268    Completed,
269    Failed,
270}
271
272/// Top-level error used when the CLI fails to talk to the daemon.
273#[derive(Debug, Error)]
274pub enum DaemonError {
275    #[error("daemon rejected command: {0}")]
276    Rejected(String),
277    #[error("transport error: {0}")]
278    Transport(String),
279}
280
281/// Client-facing trait. Implementations may talk to the daemon over IPC or
282/// shortcut calls in-process for tests.
283#[async_trait]
284pub trait DaemonService: Send + Sync {
285    async fn execute(&self, command: DaemonCommand) -> Result<DaemonResponse, DaemonError>;
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use std::collections::HashMap;
292
293    #[test]
294    fn serde_roundtrip() {
295        let response = DaemonResponse::RepoStatus(RepoStatusDetail {
296            repo_path: PathBuf::from("/tmp/test"),
297            dirty_paths: vec![PathBuf::from("file.txt")],
298            generation: 1,
299        });
300        let serialized = serde_json::to_string(&response).expect("serialize");
301        let roundtrip: DaemonResponse =
302            serde_json::from_str(&serialized).expect("deserialize from json");
303        assert_eq!(response, roundtrip);
304    }
305
306    #[test]
307    fn serde_roundtrip_metrics() {
308        let mut jobs = HashMap::new();
309        let counts = JobMetrics {
310            spawned: 3,
311            completed: 2,
312            failed: 0,
313        };
314        jobs.insert(JobKind::Prefetch, counts);
315        let response = DaemonResponse::Metrics(DaemonMetrics {
316            jobs,
317            global: GlobalMetrics {
318                pending_jobs: 1,
319                uptime_seconds: 2,
320                cpu_percent: 3.0,
321                rss_bytes: 4,
322            },
323            repos: vec![RepoMetrics {
324                repo_path: PathBuf::from("/tmp"),
325                pending_jobs: 1,
326            }],
327        });
328        let serialized = serde_json::to_string(&response).expect("serialize metrics");
329        let roundtrip: DaemonResponse =
330            serde_json::from_str(&serialized).expect("deserialize metrics");
331        assert_eq!(response, roundtrip);
332    }
333
334    #[test]
335    fn serde_roundtrip_watch_notification() {
336        let notification = DaemonNotification::WatchEvent(WatchEventNotification {
337            repo_path: PathBuf::from("/tmp/demo"),
338            path: PathBuf::from("file.txt"),
339            kind: WatchEventKind::Modified,
340        });
341        let serialized =
342            serde_json::to_string(&notification).expect("serialize watch notification");
343        let roundtrip: DaemonNotification =
344            serde_json::from_str(&serialized).expect("deserialize watch notification");
345        assert_eq!(notification, roundtrip);
346    }
347
348    #[test]
349    fn serde_roundtrip_job_notification() {
350        let notification = DaemonNotification::JobEvent(JobEventNotification {
351            repo_path: PathBuf::from("/tmp/demo"),
352            job: JobKind::Prefetch,
353            kind: JobEventKind::Started,
354        });
355        let serialized = serde_json::to_string(&notification).expect("serialize job notification");
356        let roundtrip: DaemonNotification =
357            serde_json::from_str(&serialized).expect("deserialize job notification");
358        assert_eq!(notification, roundtrip);
359    }
360
361    #[test]
362    fn serde_roundtrip_status_notification() {
363        let detail = RepoStatusDetail {
364            repo_path: PathBuf::from("/tmp/demo"),
365            dirty_paths: vec![PathBuf::from("file.txt")],
366            generation: 7,
367        };
368        let notification = DaemonNotification::RepoStatus(detail.clone());
369        let serialized =
370            serde_json::to_string(&notification).expect("serialize status notification");
371        let roundtrip: DaemonNotification =
372            serde_json::from_str(&serialized).expect("deserialize status notification");
373        assert_eq!(notification, roundtrip);
374        if let DaemonNotification::RepoStatus(decoded) = roundtrip {
375            assert_eq!(decoded, detail);
376        } else {
377            panic!("expected status notification");
378        }
379    }
380}
381/// Structured log entry emitted by the daemon.
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct LogEntry {
384    pub repo_path: PathBuf,
385    pub message: String,
386    pub timestamp: SystemTime,
387}