1use std::collections::VecDeque;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::Arc;
21
22use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct ProofTreeUpdate {
29 pub session_id: String,
31 pub goal: String,
33 pub depth: usize,
35 pub resolved: bool,
37 pub peer: Option<String>,
39 pub seq: u64,
41 pub is_final: bool,
43}
44
45pub struct ProofTreeUpdateSink {
49 sender: UnboundedSender<ProofTreeUpdate>,
50}
51
52impl ProofTreeUpdateSink {
53 pub fn new() -> (Self, UnboundedReceiver<ProofTreeUpdate>) {
55 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
56 (Self { sender }, receiver)
57 }
58
59 pub fn send(&self, update: ProofTreeUpdate) {
61 let _ = self.sender.send(update);
63 }
64
65 pub fn is_closed(&self) -> bool {
67 self.sender.is_closed()
68 }
69}
70
71pub struct ProofTreeStreamer {
75 session_id: String,
76 seq: AtomicU64,
77 sink: Arc<ProofTreeUpdateSink>,
78}
79
80impl ProofTreeStreamer {
81 pub fn new(session_id: impl Into<String>, sink: Arc<ProofTreeUpdateSink>) -> Self {
83 Self {
84 session_id: session_id.into(),
85 seq: AtomicU64::new(0),
86 sink,
87 }
88 }
89
90 pub fn session_id(&self) -> &str {
92 &self.session_id
93 }
94
95 pub fn emit_resolved(&self, goal: &str, depth: usize, peer: Option<String>) {
97 let seq = self.next_seq();
98 self.sink.send(ProofTreeUpdate {
99 session_id: self.session_id.clone(),
100 goal: goal.to_string(),
101 depth,
102 resolved: true,
103 peer,
104 seq,
105 is_final: false,
106 });
107 }
108
109 pub fn emit_unresolved(&self, goal: &str, depth: usize) {
111 let seq = self.next_seq();
112 self.sink.send(ProofTreeUpdate {
113 session_id: self.session_id.clone(),
114 goal: goal.to_string(),
115 depth,
116 resolved: false,
117 peer: None,
118 seq,
119 is_final: false,
120 });
121 }
122
123 pub fn emit_final(&self, goal: &str, depth: usize, resolved: bool, peer: Option<String>) {
125 let seq = self.next_seq();
126 self.sink.send(ProofTreeUpdate {
127 session_id: self.session_id.clone(),
128 goal: goal.to_string(),
129 depth,
130 resolved,
131 peer,
132 seq,
133 is_final: true,
134 });
135 }
136
137 pub fn stream_tree(&self, tree: &crate::proof_tree::ProofTree) {
141 let mut queue: VecDeque<&crate::proof_tree::ProofNode> = VecDeque::new();
142 queue.push_back(&tree.root);
143
144 while let Some(node) = queue.pop_front() {
145 let goal_str = format!("{}", node.goal);
146 if node.resolved {
147 self.emit_resolved(&goal_str, node.depth, node.peer.clone());
148 } else {
149 self.emit_unresolved(&goal_str, node.depth);
150 }
151 for child in &node.children {
152 queue.push_back(child);
153 }
154 }
155
156 let root_goal = format!("{}", tree.query);
158 self.emit_final(&root_goal, 0, tree.is_complete, None);
159 }
160
161 pub fn seq(&self) -> u64 {
163 self.seq.load(Ordering::SeqCst)
164 }
165
166 fn next_seq(&self) -> u64 {
169 self.seq.fetch_add(1, Ordering::SeqCst)
172 }
173}
174
175#[derive(Debug, Clone)]
179pub struct ProofTreeStreamSummary {
180 pub session_id: String,
182 pub total_updates: u64,
184 pub resolved_count: u64,
186 pub unresolved_count: u64,
188 pub contributing_peers: Vec<String>,
191 pub max_depth: usize,
193 pub is_complete: bool,
195}
196
197impl ProofTreeStreamSummary {
198 pub fn new(session_id: impl Into<String>) -> Self {
200 Self {
201 session_id: session_id.into(),
202 total_updates: 0,
203 resolved_count: 0,
204 unresolved_count: 0,
205 contributing_peers: Vec::new(),
206 max_depth: 0,
207 is_complete: false,
208 }
209 }
210
211 pub fn apply(&mut self, update: &ProofTreeUpdate) {
213 self.total_updates += 1;
214
215 if update.resolved {
216 self.resolved_count += 1;
217 } else {
218 self.unresolved_count += 1;
219 }
220
221 if let Some(ref peer) = update.peer {
222 self.contributing_peers.push(peer.clone());
223 }
224
225 if update.depth > self.max_depth {
226 self.max_depth = update.depth;
227 }
228
229 if update.is_final {
230 self.is_complete = true;
231 }
232 }
233
234 pub fn dedup_peers(&mut self) {
236 self.contributing_peers.sort_unstable();
237 self.contributing_peers.dedup();
238 }
239}
240
241#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::ir::Term;
247 use crate::proof_tree::{ProofNode, ProofTree};
248 use std::collections::HashMap;
249
250 fn make_streamer(id: &str) -> (ProofTreeStreamer, UnboundedReceiver<ProofTreeUpdate>) {
253 let (sink, rx) = ProofTreeUpdateSink::new();
254 let streamer = ProofTreeStreamer::new(id, Arc::new(sink));
255 (streamer, rx)
256 }
257
258 fn atom(s: &str) -> Term {
259 Term::Const(crate::ir::Constant::String(s.to_string()))
260 }
261
262 fn drain(rx: &mut UnboundedReceiver<ProofTreeUpdate>) -> Vec<ProofTreeUpdate> {
265 let mut updates = Vec::new();
266 while let Ok(u) = rx.try_recv() {
267 updates.push(u);
268 }
269 updates
270 }
271
272 #[test]
275 fn test_emit_resolved_increments_seq() {
276 let (streamer, mut rx) = make_streamer("session-1");
277 streamer.emit_resolved("a(X)", 0, None);
278 streamer.emit_resolved("b(X)", 1, None);
279 streamer.emit_resolved("c(X)", 2, None);
280
281 let updates = drain(&mut rx);
282 assert_eq!(updates.len(), 3);
283 assert_eq!(streamer.seq(), 3);
284 assert_eq!(updates[0].seq, 0);
285 assert_eq!(updates[1].seq, 1);
286 assert_eq!(updates[2].seq, 2);
287 }
288
289 #[test]
290 fn test_emit_unresolved() {
291 let (streamer, mut rx) = make_streamer("session-2");
292 streamer.emit_unresolved("fail(X)", 1);
293
294 let updates = drain(&mut rx);
295 assert_eq!(updates.len(), 1);
296 assert!(
297 !updates[0].resolved,
298 "unresolved update should have resolved=false"
299 );
300 assert!(updates[0].peer.is_none());
301 assert!(!updates[0].is_final);
302 }
303
304 #[test]
305 fn test_emit_final_is_final() {
306 let (streamer, mut rx) = make_streamer("session-3");
307 streamer.emit_final("query(a)", 0, true, None);
308
309 let updates = drain(&mut rx);
310 assert_eq!(updates.len(), 1);
311 assert!(updates[0].is_final, "final update must have is_final=true");
312 assert!(updates[0].resolved);
313 }
314
315 #[test]
318 fn test_stream_tree_bfs_order() {
319 let child1 = ProofNode::fact(atom("B"), 1, None);
321 let child2 = ProofNode::fact(atom("C"), 1, None);
322 let root = ProofNode {
323 goal: atom("A"),
324 rule_cid: None,
325 peer: None,
326 children: vec![child1, child2],
327 resolved: true,
328 depth: 0,
329 };
330 let tree = ProofTree {
331 root,
332 query: atom("A"),
333 bindings: HashMap::new(),
334 is_complete: true,
335 };
336
337 let (streamer, mut rx) = make_streamer("session-4");
338 streamer.stream_tree(&tree);
339
340 let updates = drain(&mut rx);
341 assert_eq!(
343 updates.len(),
344 4,
345 "expected 3 node updates + 1 final, got {}",
346 updates.len()
347 );
348
349 for u in &updates[..3] {
351 assert!(!u.is_final, "only the 4th update should be final");
352 }
353 assert!(updates[3].is_final, "last update must be final");
355 }
356
357 #[test]
360 fn test_summary_apply_counts() {
361 let mut summary = ProofTreeStreamSummary::new("session-5");
362
363 let make = |resolved: bool, is_final: bool| ProofTreeUpdate {
364 session_id: "session-5".to_string(),
365 goal: "g".to_string(),
366 depth: 0,
367 resolved,
368 peer: None,
369 seq: 0,
370 is_final,
371 };
372
373 for _ in 0..3 {
374 summary.apply(&make(true, false));
375 }
376 for _ in 0..2 {
377 summary.apply(&make(false, false));
378 }
379
380 assert_eq!(summary.total_updates, 5);
381 assert_eq!(summary.resolved_count, 3);
382 assert_eq!(summary.unresolved_count, 2);
383 assert!(!summary.is_complete, "no final update yet");
384 }
385
386 #[test]
387 fn test_summary_contributing_peers() {
388 let mut summary = ProofTreeStreamSummary::new("session-6");
389
390 let peers = ["peer-A", "peer-B", "peer-A", "peer-C", "peer-B"];
391 for peer in &peers {
392 summary.apply(&ProofTreeUpdate {
393 session_id: "session-6".to_string(),
394 goal: "g".to_string(),
395 depth: 0,
396 resolved: true,
397 peer: Some(peer.to_string()),
398 seq: 0,
399 is_final: false,
400 });
401 }
402
403 summary.dedup_peers();
404 assert_eq!(
405 summary.contributing_peers.len(),
406 3,
407 "dedup should leave 3 unique peers"
408 );
409 assert_eq!(
410 summary.contributing_peers,
411 vec!["peer-A", "peer-B", "peer-C"]
412 );
413 }
414
415 #[test]
416 fn test_summary_is_complete_after_final() {
417 let mut summary = ProofTreeStreamSummary::new("session-7");
418
419 let non_final = ProofTreeUpdate {
420 session_id: "session-7".to_string(),
421 goal: "g".to_string(),
422 depth: 0,
423 resolved: true,
424 peer: None,
425 seq: 0,
426 is_final: false,
427 };
428 summary.apply(&non_final);
429 assert!(!summary.is_complete, "should not be complete before final");
430
431 let final_update = ProofTreeUpdate {
432 is_final: true,
433 seq: 1,
434 ..non_final
435 };
436 summary.apply(&final_update);
437 assert!(summary.is_complete, "should be complete after final update");
438 }
439
440 #[test]
441 fn test_sink_closed_no_panic() {
442 let (sink, rx) = ProofTreeUpdateSink::new();
443 drop(rx);
445
446 let streamer = ProofTreeStreamer::new("session-8", Arc::new(sink));
447 streamer.emit_resolved("goal", 0, None);
449 streamer.emit_unresolved("goal", 0);
450 streamer.emit_final("goal", 0, true, None);
451 }
453
454 #[test]
455 fn test_session_id_propagated() {
456 let session = "my-unique-session-id";
457 let (streamer, mut rx) = make_streamer(session);
458
459 streamer.emit_resolved("a", 0, None);
460 streamer.emit_unresolved("b", 1);
461 streamer.emit_final("c", 0, false, None);
462
463 let updates = drain(&mut rx);
464 for u in &updates {
465 assert_eq!(
466 u.session_id, session,
467 "all updates must carry the session_id '{}'",
468 session
469 );
470 }
471 }
472
473 #[test]
474 fn test_max_depth_tracking() {
475 let mut summary = ProofTreeStreamSummary::new("session-9");
476
477 let depths = [0usize, 3, 7, 2, 5];
478 for (i, &d) in depths.iter().enumerate() {
479 summary.apply(&ProofTreeUpdate {
480 session_id: "session-9".to_string(),
481 goal: "g".to_string(),
482 depth: d,
483 resolved: true,
484 peer: None,
485 seq: i as u64,
486 is_final: false,
487 });
488 }
489
490 assert_eq!(summary.max_depth, 7, "max depth should be 7");
491 }
492}