1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::mpsc::{Sender, channel};
7use std::thread;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum DomainEvent {
14 Created { id: String, payload: String },
15 Updated { id: String, payload: String },
16 Deleted { id: String },
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AsyncEventEntry {
22 pub sequence: u64,
23 pub timestamp: DateTime<Utc>,
24 pub event: DomainEvent,
25}
26
27pub struct AsyncEventStore {
29 sender: Sender<DomainEvent>,
30 sequence: std::sync::Mutex<u64>,
31 pending: std::sync::Mutex<Vec<AsyncEventEntry>>,
32}
33
34impl AsyncEventStore {
35 pub fn spawn() -> (Self, ReceiverHandle) {
37 let (tx, rx) = channel::<DomainEvent>();
38 let (result_tx, result_rx) = channel::<()>();
39
40 thread::spawn(move || {
41 let mut sequence = 0u64;
42 while let Ok(event) = rx.recv() {
43 sequence += 1;
44 let entry = AsyncEventEntry {
47 sequence,
48 timestamp: Utc::now(),
49 event,
50 };
51 thread::sleep(Duration::from_micros(10));
53 let _ = entry;
54 }
55 let _ = result_tx.send(());
56 });
57
58 let store = Self {
59 sender: tx,
60 sequence: std::sync::Mutex::new(0),
61 pending: std::sync::Mutex::new(Vec::new()),
62 };
63 (store, ReceiverHandle { _rx: result_rx })
64 }
65
66 pub fn submit(&self, event: DomainEvent) -> Result<(), String> {
68 self.sender
69 .send(event)
70 .map_err(|e| format!("channel closed: {e}"))
71 }
72
73 pub fn sequence(&self) -> u64 {
75 *self.sequence.lock().unwrap()
76 }
77
78 pub fn increment_sequence(&self) -> u64 {
80 let mut seq = self.sequence.lock().unwrap();
81 *seq += 1;
82 *seq
83 }
84}
85
86pub struct ReceiverHandle {
88 _rx: std::sync::mpsc::Receiver<()>,
89}
90
91pub struct BatchedEventAppender {
93 sender: Sender<Vec<DomainEvent>>,
94 buffer: std::sync::Mutex<Vec<DomainEvent>>,
95 batch_size: usize,
96}
97
98impl BatchedEventAppender {
99 pub fn spawn(batch_size: usize) -> (Self, ReceiverHandle) {
101 let (tx, rx) = channel::<Vec<DomainEvent>>();
102 let (done_tx, done_rx) = channel::<()>();
103
104 thread::spawn(move || {
105 while let Ok(batch) = rx.recv() {
106 thread::sleep(Duration::from_micros(50 * batch.len() as u64));
108 }
109 let _ = done_tx.send(());
110 });
111
112 let appender = Self {
113 sender: tx,
114 buffer: std::sync::Mutex::new(Vec::new()),
115 batch_size,
116 };
117 (appender, ReceiverHandle { _rx: done_rx })
118 }
119
120 pub fn append(&self, event: DomainEvent) {
122 let mut buffer = self.buffer.lock().unwrap();
123 buffer.push(event);
124 if buffer.len() >= self.batch_size {
125 let batch: Vec<_> = buffer.drain(..).collect();
126 let _ = self.sender.send(batch);
127 }
128 }
129
130 pub fn flush(&self) {
132 let mut buffer = self.buffer.lock().unwrap();
133 if !buffer.is_empty() {
134 let batch: Vec<_> = buffer.drain(..).collect();
135 let _ = self.sender.send(batch);
136 }
137 }
138
139 pub fn pending(&self) -> usize {
141 self.buffer.lock().unwrap().len()
142 }
143}
144
145#[derive(Default)]
147pub struct AsyncProjection {
148 state: HashMap<String, String>,
149}
150
151impl AsyncProjection {
152 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub fn apply(&mut self, entry: &AsyncEventEntry) {
158 match &entry.event {
159 DomainEvent::Created { id, payload } => {
160 self.state.insert(id.clone(), payload.clone());
161 }
162 DomainEvent::Updated { id, payload } => {
163 self.state.insert(id.clone(), payload.clone());
164 }
165 DomainEvent::Deleted { id } => {
166 self.state.remove(id);
167 }
168 }
169 }
170
171 pub fn get(&self, id: &str) -> Option<String> {
173 self.state.get(id).cloned()
174 }
175
176 pub fn size(&self) -> usize {
178 self.state.len()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn async_submit_event() {
188 let (store, _handle) = AsyncEventStore::spawn();
189 store
190 .submit(DomainEvent::Created {
191 id: "x".into(),
192 payload: "data".into(),
193 })
194 .unwrap();
195 thread::sleep(Duration::from_millis(20));
196 }
198
199 #[test]
200 fn async_submit_many_events() {
201 let (store, _handle) = AsyncEventStore::spawn();
202 for i in 0..100 {
203 store
204 .submit(DomainEvent::Created {
205 id: format!("k{i}"),
206 payload: format!("v{i}"),
207 })
208 .unwrap();
209 }
210 thread::sleep(Duration::from_millis(50));
211 }
212
213 #[test]
214 fn batched_appender_buffers() {
215 let (appender, _handle) = BatchedEventAppender::spawn(5);
216 for i in 0..3 {
217 appender.append(DomainEvent::Created {
218 id: format!("k{i}"),
219 payload: "v".into(),
220 });
221 }
222 assert_eq!(appender.pending(), 3);
223 }
224
225 #[test]
226 fn batched_appender_flushes_at_batch_size() {
227 let (appender, _handle) = BatchedEventAppender::spawn(3);
228 for i in 0..5 {
229 appender.append(DomainEvent::Created {
230 id: format!("k{i}"),
231 payload: "v".into(),
232 });
233 }
234 assert_eq!(appender.pending(), 2); }
236
237 #[test]
238 fn batched_appender_explicit_flush() {
239 let (appender, _handle) = BatchedEventAppender::spawn(10);
240 appender.append(DomainEvent::Created {
241 id: "k1".into(),
242 payload: "v".into(),
243 });
244 assert_eq!(appender.pending(), 1);
245 appender.flush();
246 assert_eq!(appender.pending(), 0);
247 }
248
249 #[test]
250 fn projection_applies_events() {
251 let mut proj = AsyncProjection::new();
252 proj.apply(&AsyncEventEntry {
253 sequence: 1,
254 timestamp: Utc::now(),
255 event: DomainEvent::Created {
256 id: "a".into(),
257 payload: "1".into(),
258 },
259 });
260 proj.apply(&AsyncEventEntry {
261 sequence: 2,
262 timestamp: Utc::now(),
263 event: DomainEvent::Updated {
264 id: "a".into(),
265 payload: "2".into(),
266 },
267 });
268 assert_eq!(proj.get("a"), Some("2".into()));
269 }
270
271 #[test]
272 fn projection_delete() {
273 let mut proj = AsyncProjection::new();
274 proj.apply(&AsyncEventEntry {
275 sequence: 1,
276 timestamp: Utc::now(),
277 event: DomainEvent::Created {
278 id: "a".into(),
279 payload: "x".into(),
280 },
281 });
282 proj.apply(&AsyncEventEntry {
283 sequence: 2,
284 timestamp: Utc::now(),
285 event: DomainEvent::Deleted { id: "a".into() },
286 });
287 assert!(proj.get("a").is_none());
288 }
289}