1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub enum SyncPhase {
11 Handshake,
13 Discovery,
15 Transfer,
17 Verification,
19 Complete,
21 Failed { reason: String },
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum SyncDirection {
28 Push,
30 Pull,
32 Bidirectional,
34}
35
36#[derive(Clone, Debug)]
38pub struct SyncSession {
39 pub session_id: u64,
41 pub peer_id: String,
43 pub direction: SyncDirection,
45 pub phase: SyncPhase,
47 pub have_cids: Vec<String>,
49 pub want_cids: Vec<String>,
51 pub transferred_bytes: u64,
53 pub started_at_tick: u64,
55}
56
57impl SyncSession {
58 pub fn is_terminal(&self) -> bool {
60 matches!(self.phase, SyncPhase::Complete | SyncPhase::Failed { .. })
61 }
62
63 pub fn pending_count(&self) -> usize {
65 self.want_cids.len()
66 }
67}
68
69#[derive(Clone, Debug, Default)]
71pub struct SyncStats {
72 pub total_sessions: usize,
74 pub active_sessions: usize,
76 pub completed_sessions: usize,
78 pub failed_sessions: usize,
80 pub total_transferred_bytes: u64,
82}
83
84#[derive(Debug, Default)]
86pub struct PeerSyncCoordinator {
87 pub sessions: HashMap<u64, SyncSession>,
89 pub next_session_id: u64,
91}
92
93impl PeerSyncCoordinator {
94 pub fn new() -> Self {
96 Self {
97 sessions: HashMap::new(),
98 next_session_id: 0,
99 }
100 }
101
102 pub fn start_session(&mut self, peer_id: &str, direction: SyncDirection, tick: u64) -> u64 {
106 let session_id = self.next_session_id;
107 self.next_session_id += 1;
108
109 let session = SyncSession {
110 session_id,
111 peer_id: peer_id.to_owned(),
112 direction,
113 phase: SyncPhase::Handshake,
114 have_cids: Vec::new(),
115 want_cids: Vec::new(),
116 transferred_bytes: 0,
117 started_at_tick: tick,
118 };
119
120 self.sessions.insert(session_id, session);
121 session_id
122 }
123
124 pub fn advance_phase(&mut self, session_id: u64, next_phase: SyncPhase) -> bool {
130 match self.sessions.get_mut(&session_id) {
131 Some(session) if !session.is_terminal() => {
132 session.phase = next_phase;
133 true
134 }
135 _ => false,
136 }
137 }
138
139 pub fn add_want(&mut self, session_id: u64, cid: &str) -> bool {
143 match self.sessions.get_mut(&session_id) {
144 Some(session) if !session.is_terminal() => {
145 session.want_cids.push(cid.to_owned());
146 true
147 }
148 _ => false,
149 }
150 }
151
152 pub fn mark_received(&mut self, session_id: u64, cid: &str, bytes: u64) -> bool {
158 match self.sessions.get_mut(&session_id) {
159 Some(session) => {
160 session.want_cids.retain(|c| c != cid);
161 if !session.have_cids.contains(&cid.to_owned()) {
162 session.have_cids.push(cid.to_owned());
163 }
164 session.transferred_bytes += bytes;
165 true
166 }
167 None => false,
168 }
169 }
170
171 pub fn fail_session(&mut self, session_id: u64, reason: String) -> bool {
175 match self.sessions.get_mut(&session_id) {
176 Some(session) => {
177 session.phase = SyncPhase::Failed { reason };
178 true
179 }
180 None => false,
181 }
182 }
183
184 pub fn complete_session(&mut self, session_id: u64) -> bool {
188 match self.sessions.get_mut(&session_id) {
189 Some(session) => {
190 session.phase = SyncPhase::Complete;
191 true
192 }
193 None => false,
194 }
195 }
196
197 pub fn active_sessions(&self) -> Vec<&SyncSession> {
199 let mut active: Vec<&SyncSession> = self
200 .sessions
201 .values()
202 .filter(|s| !s.is_terminal())
203 .collect();
204 active.sort_by_key(|s| s.session_id);
205 active
206 }
207
208 pub fn session(&self, session_id: u64) -> Option<&SyncSession> {
210 self.sessions.get(&session_id)
211 }
212
213 pub fn stats(&self) -> SyncStats {
215 let total_sessions = self.sessions.len();
216 let mut active_sessions: usize = 0;
217 let mut completed_sessions: usize = 0;
218 let mut failed_sessions: usize = 0;
219 let mut total_transferred_bytes: u64 = 0;
220
221 for session in self.sessions.values() {
222 total_transferred_bytes += session.transferred_bytes;
223
224 match &session.phase {
225 SyncPhase::Complete => completed_sessions += 1,
226 SyncPhase::Failed { .. } => failed_sessions += 1,
227 _ => active_sessions += 1,
228 }
229 }
230
231 SyncStats {
232 total_sessions,
233 active_sessions,
234 completed_sessions,
235 failed_sessions,
236 total_transferred_bytes,
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn coordinator() -> PeerSyncCoordinator {
248 PeerSyncCoordinator::new()
249 }
250
251 #[test]
254 fn test_new_starts_empty() {
255 let c = coordinator();
256 assert!(c.sessions.is_empty());
257 assert_eq!(c.next_session_id, 0);
258 }
259
260 #[test]
263 fn test_start_session_creates_in_handshake() {
264 let mut c = coordinator();
265 let id = c.start_session("peer-a", SyncDirection::Pull, 10);
266 let s = c.session(id).expect("session must exist");
267 assert_eq!(s.phase, SyncPhase::Handshake);
268 assert_eq!(s.peer_id, "peer-a");
269 assert_eq!(s.started_at_tick, 10);
270 }
271
272 #[test]
275 fn test_start_session_increments_id() {
276 let mut c = coordinator();
277 let id0 = c.start_session("peer-a", SyncDirection::Push, 0);
278 let id1 = c.start_session("peer-b", SyncDirection::Pull, 1);
279 assert_eq!(id1, id0 + 1);
280 }
281
282 #[test]
285 fn test_advance_phase_succeeds() {
286 let mut c = coordinator();
287 let id = c.start_session("p", SyncDirection::Bidirectional, 0);
288 assert!(c.advance_phase(id, SyncPhase::Discovery));
289 assert_eq!(
290 c.session(id)
291 .expect("test: session must exist after advance_phase")
292 .phase,
293 SyncPhase::Discovery
294 );
295 }
296
297 #[test]
300 fn test_advance_phase_unknown_session() {
301 let mut c = coordinator();
302 assert!(!c.advance_phase(999, SyncPhase::Transfer));
303 }
304
305 #[test]
308 fn test_advance_phase_fails_on_terminal() {
309 let mut c = coordinator();
310 let id = c.start_session("p", SyncDirection::Pull, 0);
311 c.complete_session(id);
312 assert!(!c.advance_phase(id, SyncPhase::Transfer));
313 }
314
315 #[test]
318 fn test_add_want_appends() {
319 let mut c = coordinator();
320 let id = c.start_session("p", SyncDirection::Pull, 0);
321 assert!(c.add_want(id, "cid-1"));
322 assert!(c.add_want(id, "cid-2"));
323 let s = c.session(id).expect("test: session must exist");
324 assert_eq!(s.want_cids, vec!["cid-1", "cid-2"]);
325 }
326
327 #[test]
330 fn test_add_want_unknown() {
331 let mut c = coordinator();
332 assert!(!c.add_want(999, "cid-x"));
333 }
334
335 #[test]
338 fn test_mark_received_moves_cid() {
339 let mut c = coordinator();
340 let id = c.start_session("p", SyncDirection::Pull, 0);
341 c.add_want(id, "cid-1");
342 c.add_want(id, "cid-2");
343 assert!(c.mark_received(id, "cid-1", 512));
344 let s = c.session(id).expect("test: session must exist");
345 assert!(!s.want_cids.contains(&"cid-1".to_owned()));
346 assert!(s.have_cids.contains(&"cid-1".to_owned()));
347 assert!(s.want_cids.contains(&"cid-2".to_owned()));
348 }
349
350 #[test]
353 fn test_mark_received_accumulates_bytes() {
354 let mut c = coordinator();
355 let id = c.start_session("p", SyncDirection::Pull, 0);
356 c.add_want(id, "cid-a");
357 c.add_want(id, "cid-b");
358 c.mark_received(id, "cid-a", 100);
359 c.mark_received(id, "cid-b", 200);
360 assert_eq!(
361 c.session(id)
362 .expect("test: session must exist")
363 .transferred_bytes,
364 300
365 );
366 }
367
368 #[test]
371 fn test_mark_received_unknown() {
372 let mut c = coordinator();
373 assert!(!c.mark_received(999, "cid-x", 0));
374 }
375
376 #[test]
379 fn test_fail_session_sets_reason() {
380 let mut c = coordinator();
381 let id = c.start_session("p", SyncDirection::Push, 0);
382 assert!(c.fail_session(id, "timeout".to_owned()));
383 match &c
384 .session(id)
385 .expect("test: session must exist after fail_session")
386 .phase
387 {
388 SyncPhase::Failed { reason } => assert_eq!(reason, "timeout"),
389 other => panic!("expected Failed, got {other:?}"),
390 }
391 }
392
393 #[test]
396 fn test_fail_session_unknown() {
397 let mut c = coordinator();
398 assert!(!c.fail_session(999, "oops".to_owned()));
399 }
400
401 #[test]
404 fn test_complete_session() {
405 let mut c = coordinator();
406 let id = c.start_session("p", SyncDirection::Bidirectional, 0);
407 assert!(c.complete_session(id));
408 assert_eq!(
409 c.session(id)
410 .expect("test: session must exist after complete_session")
411 .phase,
412 SyncPhase::Complete
413 );
414 }
415
416 #[test]
419 fn test_is_terminal_complete() {
420 let mut c = coordinator();
421 let id = c.start_session("p", SyncDirection::Pull, 0);
422 c.complete_session(id);
423 assert!(c
424 .session(id)
425 .expect("test: session must exist after complete_session")
426 .is_terminal());
427 }
428
429 #[test]
432 fn test_is_terminal_failed() {
433 let mut c = coordinator();
434 let id = c.start_session("p", SyncDirection::Pull, 0);
435 c.fail_session(id, "error".to_owned());
436 assert!(c
437 .session(id)
438 .expect("test: session must exist after fail_session")
439 .is_terminal());
440 }
441
442 #[test]
445 fn test_is_terminal_false_for_transfer() {
446 let mut c = coordinator();
447 let id = c.start_session("p", SyncDirection::Pull, 0);
448 c.advance_phase(id, SyncPhase::Transfer);
449 assert!(!c
450 .session(id)
451 .expect("test: session must exist after advance_phase")
452 .is_terminal());
453 }
454
455 #[test]
458 fn test_active_sessions_excludes_terminal() {
459 let mut c = coordinator();
460 let id0 = c.start_session("p0", SyncDirection::Pull, 0);
461 let id1 = c.start_session("p1", SyncDirection::Push, 1);
462 c.complete_session(id0);
463 let active = c.active_sessions();
464 let ids: Vec<u64> = active.iter().map(|s| s.session_id).collect();
465 assert!(!ids.contains(&id0));
466 assert!(ids.contains(&id1));
467 }
468
469 #[test]
472 fn test_active_sessions_sorted() {
473 let mut c = coordinator();
474 let id2 = c.start_session("p2", SyncDirection::Pull, 0);
476 let id1 = c.start_session("p1", SyncDirection::Pull, 1);
477 let id0 = c.start_session("p0", SyncDirection::Pull, 2);
478 let active = c.active_sessions();
479 let ids: Vec<u64> = active.iter().map(|s| s.session_id).collect();
480 assert_eq!(ids, vec![id2, id1, id0]);
482 }
483
484 #[test]
487 fn test_stats_total_sessions() {
488 let mut c = coordinator();
489 c.start_session("p0", SyncDirection::Pull, 0);
490 c.start_session("p1", SyncDirection::Push, 1);
491 assert_eq!(c.stats().total_sessions, 2);
492 }
493
494 #[test]
497 fn test_stats_completed_and_failed() {
498 let mut c = coordinator();
499 let id0 = c.start_session("p0", SyncDirection::Pull, 0);
500 let id1 = c.start_session("p1", SyncDirection::Push, 1);
501 let _id2 = c.start_session("p2", SyncDirection::Bidirectional, 2);
502 c.complete_session(id0);
503 c.fail_session(id1, "err".to_owned());
504 let stats = c.stats();
505 assert_eq!(stats.completed_sessions, 1);
506 assert_eq!(stats.failed_sessions, 1);
507 assert_eq!(stats.active_sessions, 1);
508 }
509
510 #[test]
513 fn test_stats_total_transferred_bytes() {
514 let mut c = coordinator();
515 let id0 = c.start_session("p0", SyncDirection::Pull, 0);
516 let id1 = c.start_session("p1", SyncDirection::Pull, 1);
517 c.add_want(id0, "cid-a");
518 c.add_want(id1, "cid-b");
519 c.mark_received(id0, "cid-a", 1000);
520 c.mark_received(id1, "cid-b", 2500);
521 assert_eq!(c.stats().total_transferred_bytes, 3500);
522 }
523
524 #[test]
527 fn test_pending_count() {
528 let mut c = coordinator();
529 let id = c.start_session("p", SyncDirection::Pull, 0);
530 assert_eq!(
531 c.session(id)
532 .expect("test: session must exist")
533 .pending_count(),
534 0
535 );
536 c.add_want(id, "cid-1");
537 c.add_want(id, "cid-2");
538 assert_eq!(
539 c.session(id)
540 .expect("test: session must exist")
541 .pending_count(),
542 2
543 );
544 c.mark_received(id, "cid-1", 0);
545 assert_eq!(
546 c.session(id)
547 .expect("test: session must exist after mark_received")
548 .pending_count(),
549 1
550 );
551 }
552
553 #[test]
556 fn test_add_want_terminal_returns_false() {
557 let mut c = coordinator();
558 let id = c.start_session("p", SyncDirection::Push, 0);
559 c.complete_session(id);
560 assert!(!c.add_want(id, "cid-x"));
561 }
562
563 #[test]
566 fn test_advance_phase_on_failed_terminal() {
567 let mut c = coordinator();
568 let id = c.start_session("p", SyncDirection::Pull, 0);
569 c.fail_session(id, "err".to_owned());
570 assert!(!c.advance_phase(id, SyncPhase::Complete));
571 assert!(matches!(
573 c.session(id).expect("test: session must exist").phase,
574 SyncPhase::Failed { .. }
575 ));
576 }
577}