1use std::collections::HashSet;
8use std::path::Path;
9use std::time::Duration;
10
11use ito_domain::audit::event::AuditEvent;
12
13use super::store::{AuditEventStore, audit_storage_location_key, default_audit_store};
14use super::worktree::discover_worktrees;
15
16#[derive(Debug, Clone)]
18pub struct StreamConfig {
19 pub poll_interval: Duration,
21 pub all_worktrees: bool,
23 pub last: usize,
25}
26
27impl Default for StreamConfig {
28 fn default() -> Self {
29 Self {
30 poll_interval: Duration::from_millis(500),
31 all_worktrees: false,
32 last: 10,
33 }
34 }
35}
36
37pub struct StreamSource {
39 pub label: String,
41 store: Box<dyn AuditEventStore>,
43 offset: usize,
49}
50
51#[derive(Debug)]
53pub struct StreamEvent {
54 pub event: AuditEvent,
56 pub source: String,
58}
59
60pub fn read_initial_events(
65 ito_path: &Path,
66 config: &StreamConfig,
67) -> (Vec<StreamEvent>, Vec<StreamSource>) {
68 let mut sources = Vec::new();
69 let mut events = Vec::new();
70 let mut seen_locations = HashSet::new();
71
72 let main_store = default_audit_store(ito_path);
74 let main_key = audit_storage_location_key(&main_store.location());
75 let main_events = main_store.read_all();
76 let start = main_events.len().saturating_sub(config.last);
77 for event in &main_events[start..] {
78 events.push(StreamEvent {
79 event: event.clone(),
80 source: "main".to_string(),
81 });
82 }
83 sources.push(StreamSource {
84 label: "main".to_string(),
85 store: main_store,
86 offset: main_events.len(),
87 });
88 seen_locations.insert(main_key);
89
90 if config.all_worktrees {
92 let worktrees = discover_worktrees(ito_path);
93 for wt in &worktrees {
94 if wt.is_main {
95 continue; }
97 let wt_ito_path = wt.path.join(".ito");
98 if !wt_ito_path.exists() {
99 continue;
100 }
101
102 let wt_store = default_audit_store(&wt_ito_path);
103 let wt_key = audit_storage_location_key(&wt_store.location());
104 if !seen_locations.insert(wt_key) {
105 continue;
106 }
107
108 let wt_events = wt_store.read_all();
109 let label = wt
110 .branch
111 .clone()
112 .unwrap_or_else(|| wt.path.display().to_string());
113 let start = wt_events.len().saturating_sub(config.last);
114 for event in &wt_events[start..] {
115 events.push(StreamEvent {
116 event: event.clone(),
117 source: label.clone(),
118 });
119 }
120 sources.push(StreamSource {
121 label,
122 store: wt_store,
123 offset: wt_events.len(),
124 });
125 }
126 }
127
128 (events, sources)
129}
130
131pub fn poll_new_events(sources: &mut [StreamSource]) -> Vec<StreamEvent> {
139 let mut new_events = Vec::new();
140
141 for source in sources.iter_mut() {
142 let current_events = source.store.read_all();
143 if current_events.len() <= source.offset {
144 continue;
145 }
146
147 for event in ¤t_events[source.offset..] {
148 new_events.push(StreamEvent {
149 event: event.clone(),
150 source: source.label.clone(),
151 });
152 }
153
154 source.offset = current_events.len();
155 }
156
157 new_events
158}
159
160#[cfg(test)]
161#[path = "stream_tests.rs"]
162mod stream_tests;