1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::sync::Mutex;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum SessionEvent {
11 Created {
12 session_id: String,
13 threshold: u32,
14 party_count: u32,
15 },
16 CommitmentSubmitted {
17 session_id: String,
18 signer_id: String,
19 },
20 ShareSubmitted {
21 session_id: String,
22 signer_id: String,
23 },
24 Completed {
25 session_id: String,
26 },
27 Expired {
28 session_id: String,
29 },
30 Aborted {
31 session_id: String,
32 reason: String,
33 },
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct EventEntry {
39 pub sequence: u64,
40 pub timestamp: DateTime<Utc>,
41 pub event: SessionEvent,
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct SessionProjection {
47 pub session_id: String,
48 pub state: String,
49 pub threshold: u32,
50 pub party_count: u32,
51 pub commitments: Vec<String>,
52 pub shares: Vec<String>,
53}
54
55#[derive(Default)]
57pub struct EventStore {
58 events: Mutex<Vec<EventEntry>>,
59 next_seq: Mutex<u64>,
60}
61
62impl EventStore {
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn append(&self, event: SessionEvent) -> u64 {
68 let seq = {
69 let mut next = self.next_seq.lock().unwrap();
70 let s = *next;
71 *next += 1;
72 s
73 };
74 self.events.lock().unwrap().push(EventEntry {
75 sequence: seq,
76 timestamp: Utc::now(),
77 event,
78 });
79 seq
80 }
81
82 pub fn all_events(&self) -> Vec<EventEntry> {
83 self.events.lock().unwrap().clone()
84 }
85
86 pub fn events_for_session(&self, session_id: &str) -> Vec<EventEntry> {
87 self.events
88 .lock()
89 .unwrap()
90 .iter()
91 .filter(|e| session_matches(&e.event, session_id))
92 .cloned()
93 .collect()
94 }
95
96 pub fn project_session(&self, session_id: &str) -> Option<SessionProjection> {
97 let events = self.events_for_session(session_id);
98 if events.is_empty() {
99 return None;
100 }
101 let mut proj = SessionProjection {
102 session_id: session_id.into(),
103 ..Default::default()
104 };
105 for entry in &events {
106 apply_event(&mut proj, &entry.event);
107 }
108 Some(proj)
109 }
110
111 pub fn project_all(&self) -> Vec<SessionProjection> {
112 let session_ids: std::collections::HashSet<String> = self
113 .events
114 .lock()
115 .unwrap()
116 .iter()
117 .filter_map(|e| session_id_of(&e.event))
118 .collect();
119 session_ids
120 .iter()
121 .filter_map(|sid| self.project_session(sid))
122 .collect()
123 }
124
125 pub fn event_count(&self) -> usize {
126 self.events.lock().unwrap().len()
127 }
128
129 pub fn replay(&self) {
130 let _ = self.all_events();
132 }
133}
134
135fn session_matches(event: &SessionEvent, session_id: &str) -> bool {
136 session_id_of(event).as_deref() == Some(session_id)
137}
138
139fn session_id_of(event: &SessionEvent) -> Option<String> {
140 match event {
141 SessionEvent::Created { session_id, .. } => Some(session_id.clone()),
142 SessionEvent::CommitmentSubmitted { session_id, .. } => Some(session_id.clone()),
143 SessionEvent::ShareSubmitted { session_id, .. } => Some(session_id.clone()),
144 SessionEvent::Completed { session_id } => Some(session_id.clone()),
145 SessionEvent::Expired { session_id } => Some(session_id.clone()),
146 SessionEvent::Aborted { session_id, .. } => Some(session_id.clone()),
147 }
148}
149
150fn apply_event(proj: &mut SessionProjection, event: &SessionEvent) {
151 match event {
152 SessionEvent::Created {
153 threshold,
154 party_count,
155 ..
156 } => {
157 proj.state = "pending".into();
158 proj.threshold = *threshold;
159 proj.party_count = *party_count;
160 }
161 SessionEvent::CommitmentSubmitted { signer_id, .. } => {
162 proj.commitments.push(signer_id.clone());
163 }
164 SessionEvent::ShareSubmitted { signer_id, .. } => {
165 proj.shares.push(signer_id.clone());
166 }
167 SessionEvent::Completed { .. } => {
168 proj.state = "completed".into();
169 }
170 SessionEvent::Expired { .. } => {
171 proj.state = "expired".into();
172 }
173 SessionEvent::Aborted { .. } => {
174 proj.state = "aborted".into();
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn append_and_read() {
185 let store = EventStore::new();
186 store.append(SessionEvent::Created {
187 session_id: "s1".into(),
188 threshold: 2,
189 party_count: 3,
190 });
191 assert_eq!(store.event_count(), 1);
192 }
193
194 #[test]
195 fn project_session() {
196 let store = EventStore::new();
197 store.append(SessionEvent::Created {
198 session_id: "s1".into(),
199 threshold: 2,
200 party_count: 3,
201 });
202 store.append(SessionEvent::CommitmentSubmitted {
203 session_id: "s1".into(),
204 signer_id: "alice".into(),
205 });
206 store.append(SessionEvent::Completed {
207 session_id: "s1".into(),
208 });
209
210 let proj = store.project_session("s1").unwrap();
211 assert_eq!(proj.state, "completed");
212 assert_eq!(proj.threshold, 2);
213 assert_eq!(proj.commitments, vec!["alice"]);
214 }
215
216 #[test]
217 fn project_all_sessions() {
218 let store = EventStore::new();
219 store.append(SessionEvent::Created {
220 session_id: "s1".into(),
221 threshold: 1,
222 party_count: 1,
223 });
224 store.append(SessionEvent::Created {
225 session_id: "s2".into(),
226 threshold: 2,
227 party_count: 3,
228 });
229 let all = store.project_all();
230 assert_eq!(all.len(), 2);
231 }
232
233 #[test]
234 fn events_for_specific_session() {
235 let store = EventStore::new();
236 store.append(SessionEvent::Created {
237 session_id: "s1".into(),
238 threshold: 2,
239 party_count: 3,
240 });
241 store.append(SessionEvent::Created {
242 session_id: "s2".into(),
243 threshold: 2,
244 party_count: 3,
245 });
246 assert_eq!(store.events_for_session("s1").len(), 1);
247 }
248
249 #[test]
250 fn sequence_numbers_monotonic() {
251 let store = EventStore::new();
252 let s1 = store.append(SessionEvent::Created {
253 session_id: "s1".into(),
254 threshold: 2,
255 party_count: 3,
256 });
257 let s2 = store.append(SessionEvent::Completed {
258 session_id: "s1".into(),
259 });
260 assert!(s1 < s2);
261 }
262
263 #[test]
264 fn project_nonexistent_returns_none() {
265 let store = EventStore::new();
266 assert!(store.project_session("nope").is_none());
267 }
268
269 #[test]
270 fn aborted_state() {
271 let store = EventStore::new();
272 store.append(SessionEvent::Created {
273 session_id: "s1".into(),
274 threshold: 2,
275 party_count: 3,
276 });
277 store.append(SessionEvent::Aborted {
278 session_id: "s1".into(),
279 reason: "test".into(),
280 });
281 let proj = store.project_session("s1").unwrap();
282 assert_eq!(proj.state, "aborted");
283 }
284
285 #[test]
286 fn multiple_commitments_tracked() {
287 let store = EventStore::new();
288 store.append(SessionEvent::Created {
289 session_id: "s1".into(),
290 threshold: 2,
291 party_count: 3,
292 });
293 store.append(SessionEvent::CommitmentSubmitted {
294 session_id: "s1".into(),
295 signer_id: "a".into(),
296 });
297 store.append(SessionEvent::CommitmentSubmitted {
298 session_id: "s1".into(),
299 signer_id: "b".into(),
300 });
301 let proj = store.project_session("s1").unwrap();
302 assert_eq!(proj.commitments.len(), 2);
303 }
304}