Skip to main content

nodedb_lite/sync/
compensation.rs

1//! Compensation handling for rejected sync deltas.
2//!
3//! When Origin rejects a delta (UNIQUE violation, RLS, rate limit), the
4//! edge receives a `DeltaRejectMsg` with a `CompensationHint`. This module
5//! rolls back the optimistic local state and notifies the application.
6
7use std::sync::{Arc, Mutex};
8
9use nodedb_types::sync::compensation::CompensationHint;
10
11/// Event delivered to the application when a delta is rejected.
12#[derive(Debug, Clone)]
13pub struct CompensationEvent {
14    /// Mutation ID that was rejected.
15    pub mutation_id: u64,
16    /// Collection the rejected operation targeted.
17    pub collection: String,
18    /// Document ID affected.
19    pub document_id: String,
20    /// Why it was rejected.
21    pub hint: CompensationHint,
22}
23
24/// Application callback for compensation events.
25///
26/// The application registers a handler to decide how to react:
27/// - Prompt the user ("username taken")
28/// - Auto-retry with a modified value
29/// - Silently accept the rejection
30pub trait CompensationHandler: Send + Sync + 'static {
31    fn on_compensation(&self, event: CompensationEvent);
32}
33
34/// Function-based compensation handler for convenience.
35impl<F: Fn(CompensationEvent) + Send + Sync + 'static> CompensationHandler for F {
36    fn on_compensation(&self, event: CompensationEvent) {
37        self(event);
38    }
39}
40
41/// Registry for compensation handlers.
42///
43/// Thread-safe — the sync client calls handlers from its background task
44/// while the application may register/deregister from the main thread.
45pub struct CompensationRegistry {
46    handler: Mutex<Option<Arc<dyn CompensationHandler>>>,
47    /// Events captured when no handler is registered (buffered for late binding).
48    buffered: Mutex<Vec<CompensationEvent>>,
49    /// Max buffer size to prevent unbounded growth.
50    max_buffer: usize,
51}
52
53impl CompensationRegistry {
54    pub fn new() -> Self {
55        Self {
56            handler: Mutex::new(None),
57            buffered: Mutex::new(Vec::new()),
58            max_buffer: 1000,
59        }
60    }
61
62    /// Register a compensation handler. Drains any buffered events to it.
63    pub fn set_handler(&self, handler: Arc<dyn CompensationHandler>) {
64        // Drain buffer first.
65        if let Ok(mut buf) = self.buffered.lock() {
66            for event in buf.drain(..) {
67                handler.on_compensation(event);
68            }
69        }
70        if let Ok(mut h) = self.handler.lock() {
71            *h = Some(handler);
72        }
73    }
74
75    /// Remove the compensation handler.
76    pub fn clear_handler(&self) {
77        if let Ok(mut h) = self.handler.lock() {
78            *h = None;
79        }
80    }
81
82    /// Dispatch a compensation event. If no handler is registered, buffer it.
83    pub fn dispatch(&self, event: CompensationEvent) {
84        if let Ok(h) = self.handler.lock()
85            && let Some(handler) = h.as_ref()
86        {
87            handler.on_compensation(event);
88            return;
89        }
90        // No handler — buffer.
91        if let Ok(mut buf) = self.buffered.lock()
92            && buf.len() < self.max_buffer
93        {
94            buf.push(event);
95        }
96    }
97
98    /// Number of buffered events (pending handler registration).
99    pub fn buffered_count(&self) -> usize {
100        self.buffered.lock().map(|b| b.len()).unwrap_or(0)
101    }
102}
103
104impl Default for CompensationRegistry {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::sync::atomic::{AtomicU32, Ordering};
114
115    fn make_event(mutation_id: u64) -> CompensationEvent {
116        CompensationEvent {
117            mutation_id,
118            collection: "users".into(),
119            document_id: "u1".into(),
120            hint: CompensationHint::UniqueViolation {
121                field: "email".into(),
122                conflicting_value: "a@b.com".into(),
123            },
124        }
125    }
126
127    #[test]
128    fn dispatch_with_handler() {
129        let count = Arc::new(AtomicU32::new(0));
130        let count_clone = count.clone();
131        let registry = CompensationRegistry::new();
132
133        registry.set_handler(Arc::new(move |_: CompensationEvent| {
134            count_clone.fetch_add(1, Ordering::Relaxed);
135        }));
136
137        registry.dispatch(make_event(1));
138        registry.dispatch(make_event(2));
139
140        assert_eq!(count.load(Ordering::Relaxed), 2);
141    }
142
143    #[test]
144    fn dispatch_without_handler_buffers() {
145        let registry = CompensationRegistry::new();
146
147        registry.dispatch(make_event(1));
148        registry.dispatch(make_event(2));
149
150        assert_eq!(registry.buffered_count(), 2);
151    }
152
153    #[test]
154    fn late_handler_drains_buffer() {
155        let count = Arc::new(AtomicU32::new(0));
156        let count_clone = count.clone();
157        let registry = CompensationRegistry::new();
158
159        // Dispatch before handler is set.
160        registry.dispatch(make_event(1));
161        registry.dispatch(make_event(2));
162        assert_eq!(registry.buffered_count(), 2);
163
164        // Set handler — should drain buffer.
165        registry.set_handler(Arc::new(move |_: CompensationEvent| {
166            count_clone.fetch_add(1, Ordering::Relaxed);
167        }));
168
169        assert_eq!(count.load(Ordering::Relaxed), 2);
170        assert_eq!(registry.buffered_count(), 0);
171
172        // New events go directly to handler.
173        registry.dispatch(make_event(3));
174        assert_eq!(count.load(Ordering::Relaxed), 3);
175    }
176
177    #[test]
178    fn clear_handler_resumes_buffering() {
179        let registry = CompensationRegistry::new();
180        let count = Arc::new(AtomicU32::new(0));
181        let count_clone = count.clone();
182
183        registry.set_handler(Arc::new(move |_: CompensationEvent| {
184            count_clone.fetch_add(1, Ordering::Relaxed);
185        }));
186
187        registry.dispatch(make_event(1));
188        assert_eq!(count.load(Ordering::Relaxed), 1);
189
190        registry.clear_handler();
191        registry.dispatch(make_event(2));
192        assert_eq!(count.load(Ordering::Relaxed), 1); // Not incremented.
193        assert_eq!(registry.buffered_count(), 1);
194    }
195}