kimun_notes/server_client/
observer.rs1use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8use kimun_core::{IndexObserver, NoteChange, nfs::VaultPath};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DirtyOp {
16 Upsert(u64),
17 Delete,
18}
19
20#[derive(Debug, Default)]
22pub struct DirtySet {
23 inner: Mutex<HashMap<VaultPath, DirtyOp>>,
24}
25
26impl DirtySet {
27 pub fn record(&self, change: &NoteChange) {
29 let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
30 match change {
31 NoteChange::Upsert { path, hash } => map.insert(path.clone(), DirtyOp::Upsert(*hash)),
32 NoteChange::Delete { path } => map.insert(path.clone(), DirtyOp::Delete),
33 };
34 }
35
36 pub fn drain(&self) -> Vec<(VaultPath, DirtyOp)> {
38 let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
39 std::mem::take(&mut *map).into_iter().collect()
40 }
41
42 pub fn requeue(&self, items: impl IntoIterator<Item = (VaultPath, DirtyOp)>) {
45 let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
46 for (path, op) in items {
47 map.entry(path).or_insert(op);
48 }
49 }
50
51 pub fn len(&self) -> usize {
52 self.inner.lock().unwrap_or_else(|e| e.into_inner()).len()
53 }
54
55 pub fn is_empty(&self) -> bool {
56 self.len() == 0
57 }
58}
59
60#[derive(Debug)]
62pub struct RagObserver {
63 dirty: Arc<DirtySet>,
64}
65
66impl RagObserver {
67 pub fn new(dirty: Arc<DirtySet>) -> Self {
68 Self { dirty }
69 }
70}
71
72impl IndexObserver for RagObserver {
73 fn on_change(&self, change: &NoteChange) {
74 self.dirty.record(change);
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 fn upsert(p: &str) -> NoteChange {
83 NoteChange::Upsert {
84 path: VaultPath::new(p),
85 hash: 1,
86 }
87 }
88 fn delete(p: &str) -> NoteChange {
89 NoteChange::Delete {
90 path: VaultPath::new(p),
91 }
92 }
93
94 #[test]
95 fn latest_op_wins_per_path() {
96 let set = DirtySet::default();
97 set.record(&upsert("a.md"));
98 set.record(&delete("a.md")); let drained = set.drain();
100 assert_eq!(drained, vec![(VaultPath::new("a.md"), DirtyOp::Delete)]);
101
102 set.record(&delete("b.md"));
103 set.record(&upsert("b.md")); assert_eq!(
105 set.drain(),
106 vec![(VaultPath::new("b.md"), DirtyOp::Upsert(1))]
107 );
108 }
109
110 #[test]
111 fn drain_clears() {
112 let set = DirtySet::default();
113 set.record(&upsert("a.md"));
114 assert_eq!(set.len(), 1);
115 let _ = set.drain();
116 assert!(set.is_empty());
117 }
118
119 #[test]
120 fn requeue_does_not_clobber_newer_op() {
121 let set = DirtySet::default();
122 set.record(&delete("a.md"));
124 set.requeue([(VaultPath::new("a.md"), DirtyOp::Upsert(1))]);
125 assert_eq!(set.drain(), vec![(VaultPath::new("a.md"), DirtyOp::Delete)]);
127 }
128
129 #[test]
130 fn observer_records_into_shared_set() {
131 let dirty = Arc::new(DirtySet::default());
132 let observer = RagObserver::new(dirty.clone());
133 observer.on_change(&upsert("n.md"));
134 assert_eq!(dirty.len(), 1);
135 }
136}