1use std::collections::{HashMap, VecDeque};
8use std::pin::Pin;
9use std::sync::Arc;
10
11use async_stream::stream;
12use async_trait::async_trait;
13use chrono::Utc;
14use dashmap::DashMap;
15use futures::stream::Stream;
16use serde_json::Value;
17use tokio::sync::{broadcast, RwLock};
18use uuid::Uuid;
19
20use super::partition::topic_filter_matches;
21use super::port::{StorageCapabilities, StoragePort};
22use crate::error::Result;
23use crate::event::{open_stored_event, seal_event_for_storage, TransportCrypto};
24use crate::models::Event;
25
26const DEFAULT_REPLAY_BUFFER_SIZE: usize = 1000;
27
28fn partition_key(topic_name: &str, topic_key: Option<&str>) -> String {
29 format!("{}:{}", topic_name, topic_key.unwrap_or("__null__"))
30}
31
32fn replay_key(topic_name: &str, topic_key_filter: Option<&str>) -> String {
33 partition_key(topic_name, topic_key_filter)
34}
35
36pub struct InProcStoragePort {
78 crypto: TransportCrypto,
79 tx: broadcast::Sender<Event>,
80 replay_buffer: Arc<RwLock<HashMap<String, VecDeque<Event>>>>,
81 replay_buffer_size: usize,
82 events: Arc<DashMap<String, Event>>,
83 seq_counters: Arc<DashMap<String, i64>>,
84 checkpoints: Arc<DashMap<String, i64>>,
85 delivery_pins: Arc<DashMap<String, i64>>,
86}
87
88impl InProcStoragePort {
89 #[must_use]
91 pub fn new(crypto: TransportCrypto) -> Self {
92 let (tx, _) = broadcast::channel(1024);
93 Self {
94 crypto,
95 tx,
96 replay_buffer: Arc::new(RwLock::new(HashMap::new())),
97 replay_buffer_size: DEFAULT_REPLAY_BUFFER_SIZE,
98 events: Arc::new(DashMap::new()),
99 seq_counters: Arc::new(DashMap::new()),
100 checkpoints: Arc::new(DashMap::new()),
101 delivery_pins: Arc::new(DashMap::new()),
102 }
103 }
104
105 fn checkpoint_key(sub: &str, topic: &str, topic_key: Option<&str>) -> String {
106 format!("{sub}:{}:{}", topic, topic_key.unwrap_or("__null__"))
107 }
108
109 #[allow(clippy::significant_drop_tightening)]
110 async fn push_replay(&self, event: &Event) {
111 let key = replay_key(&event.topic_name, event.topic_key.as_deref());
112 let event = event.clone();
113 let mut buf = self.replay_buffer.write().await;
114 let queue = buf.entry(key).or_default();
115 queue.push_back(event);
116 while queue.len() > self.replay_buffer_size {
117 queue.pop_front();
118 }
119 }
120
121 fn next_seq(&self, topic_name: &str, topic_key: Option<&str>) -> i64 {
122 let key = partition_key(topic_name, topic_key);
123 let mut entry = self.seq_counters.entry(key).or_insert(0);
124 *entry += 1;
125 *entry
126 }
127}
128
129#[async_trait]
130impl StoragePort for InProcStoragePort {
131 fn capabilities(&self) -> StorageCapabilities {
132 StorageCapabilities::mem()
133 }
134
135 async fn append(
136 &self,
137 topic_name: &str,
138 topic_key: Option<&str>,
139 actor_json: Value,
140 payload_json: Value,
141 ) -> Result<Event> {
142 let seq = self.next_seq(topic_name, topic_key);
143 let event = Event {
144 event_id: Uuid::new_v4().to_string(),
145 topic_name: topic_name.to_string(),
146 topic_key: topic_key.map(String::from),
147 seq,
148 actor_json,
149 payload_json,
150 created_at: Utc::now(),
151 };
152 let (plain, sealed) = seal_event_for_storage(&self.crypto, event)?;
153 self.events.insert(sealed.event_id.clone(), sealed.clone());
154 self.push_replay(&sealed).await;
155 let _ = self.tx.send(plain.clone());
156 Ok(plain)
157 }
158
159 fn subscribe(
160 &self,
161 topic_name: String,
162 topic_key_filter: Option<String>,
163 after_seq: Option<i64>,
164 ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
165 let replay_key = replay_key(&topic_name, topic_key_filter.as_deref());
166 let replay_buffer = Arc::clone(&self.replay_buffer);
167 let crypto = self.crypto.clone();
168 let mut live_rx = self.tx.subscribe();
169 let topic = topic_name;
170 let filter = topic_key_filter;
171 let delivery_pins = Arc::clone(&self.delivery_pins);
172
173 Box::pin(stream! {
174 if let Some(seq) = after_seq {
175 let buf = replay_buffer.read().await;
176 if let Some(queue) = buf.get(&replay_key) {
177 for evt in queue {
178 if evt.seq > seq && topic_filter_matches(evt, &topic, filter.as_ref()) {
179 yield open_stored_event(&crypto, evt.clone());
180 }
181 }
182 }
183 }
184
185 loop {
186 match live_rx.recv().await {
187 Ok(ev) => {
188 if !topic_filter_matches(&ev, &topic, filter.as_ref()) {
189 continue;
190 }
191 if after_seq.is_some_and(|s| ev.seq <= s) {
192 continue;
193 }
194 let pin_key = partition_key(&ev.topic_name, ev.topic_key.as_deref());
195 delivery_pins.insert(pin_key, ev.seq);
196 yield Ok(ev);
197 }
198 Err(broadcast::error::RecvError::Lagged(_)) => {
199 let pin_key = partition_key(&topic, filter.as_deref());
201 let after = delivery_pins
202 .get(&pin_key)
203 .map(|v| *v)
204 .or(after_seq)
205 .unwrap_or(0);
206 let buf = replay_buffer.read().await;
207 if let Some(queue) = buf.get(&replay_key) {
208 for evt in queue {
209 if evt.seq > after
210 && topic_filter_matches(evt, &topic, filter.as_ref())
211 {
212 let pk = partition_key(
213 &evt.topic_name,
214 evt.topic_key.as_deref(),
215 );
216 delivery_pins.insert(pk, evt.seq);
217 yield open_stored_event(&crypto, evt.clone());
218 }
219 }
220 }
221 }
222 Err(broadcast::error::RecvError::Closed) => break,
223 }
224 }
225 })
226 }
227
228 async fn get_event(&self, event_id: &str) -> Result<Option<Event>> {
229 self.events
230 .get(event_id)
231 .map(|event| open_stored_event(&self.crypto, event.clone()))
232 .transpose()
233 }
234
235 async fn list_by_topic(
236 &self,
237 topic_name: &str,
238 topic_key: Option<&str>,
239 after_seq: Option<i64>,
240 limit: usize,
241 ) -> Result<Vec<Event>> {
242 if limit == 0 {
243 return Ok(Vec::new());
244 }
245 let after = after_seq.unwrap_or(0);
246 let mut matched: Vec<Event> = Vec::new();
247 for entry in self.events.iter() {
248 let sealed = entry.value();
249 if sealed.topic_name != topic_name {
250 continue;
251 }
252 if topic_key.is_some_and(|k| sealed.topic_key.as_deref() != Some(k)) {
253 continue;
254 }
255 if sealed.seq <= after {
256 continue;
257 }
258 matched.push(open_stored_event(&self.crypto, sealed.clone())?);
259 }
260 matched.sort_by_key(|e| e.seq);
261 matched.truncate(limit);
262 Ok(matched)
263 }
264
265 async fn list_recent(&self, limit: usize) -> Result<Vec<Event>> {
266 if limit == 0 {
267 return Ok(Vec::new());
268 }
269 let mut matched: Vec<Event> = Vec::with_capacity(self.events.len().min(limit * 2));
270 for entry in self.events.iter() {
271 matched.push(open_stored_event(&self.crypto, entry.value().clone())?);
272 }
273 matched.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.seq.cmp(&a.seq)));
274 matched.truncate(limit);
275 Ok(matched)
276 }
277
278 async fn load_checkpoint(
279 &self,
280 subscription_name: &str,
281 topic_name: &str,
282 topic_key: Option<&str>,
283 ) -> Result<Option<i64>> {
284 let key = Self::checkpoint_key(subscription_name, topic_name, topic_key);
285 Ok(self.checkpoints.get(&key).map(|v| *v))
286 }
287
288 async fn commit_checkpoint(
289 &self,
290 subscription_name: &str,
291 topic_name: &str,
292 topic_key: Option<&str>,
293 last_seq: i64,
294 ) -> Result<()> {
295 let key = Self::checkpoint_key(subscription_name, topic_name, topic_key);
296 self.checkpoints
297 .entry(key)
298 .and_modify(|existing| *existing = (*existing).max(last_seq))
299 .or_insert(last_seq);
300 Ok(())
301 }
302
303 async fn truncate_before(
304 &self,
305 topic_name: &str,
306 topic_key: Option<&str>,
307 truncate_bound: i64,
308 ) -> Result<u64> {
309 let key = replay_key(topic_name, topic_key);
310 let mut removed = 0u64;
311 let mut buf = self.replay_buffer.write().await;
312 if let Some(queue) = buf.get_mut(&key) {
313 while queue.front().is_some_and(|e| e.seq < truncate_bound) {
314 if let Some(ev) = queue.pop_front() {
315 self.events.remove(&ev.event_id);
316 removed += 1;
317 }
318 }
319 }
320 drop(buf);
321 Ok(removed)
322 }
323
324 async fn delivery_seq_pin(&self, topic_name: &str, topic_key: Option<&str>) -> Option<i64> {
325 let key = partition_key(topic_name, topic_key);
326 self.delivery_pins.get(&key).map(|v| *v)
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[tokio::test]
335 async fn append_stores_sealed_fields_and_returns_plaintext() {
336 let port = InProcStoragePort::new(TransportCrypto::from_bytes([7; 32]));
337 let marker = "SECRET_PLAINTEXT_MARKER_xyz";
338 let appended = port
339 .append(
340 "test.sealed",
341 None,
342 serde_json::json!({"actor": "test"}),
343 serde_json::json!({"message": marker}),
344 )
345 .await
346 .expect("append");
347
348 assert!(appended.payload_json.to_string().contains(marker));
349 let stored = port.events.get(&appended.event_id).expect("stored event");
350 assert!(!stored.payload_json.to_string().contains(marker));
351
352 let fetched = port
353 .get_event(&appended.event_id)
354 .await
355 .expect("get event")
356 .expect("event");
357 assert_eq!(fetched.payload_json, appended.payload_json);
358 }
359
360 #[tokio::test]
361 async fn list_by_topic_and_list_recent_honor_limit_and_order() {
362 let port = InProcStoragePort::new(TransportCrypto::from_bytes([9; 32]));
363 let topic = "test.list";
364 let mut ids = Vec::new();
365 for i in 0..5 {
366 let ev = port
367 .append(
368 topic,
369 None,
370 serde_json::json!({}),
371 serde_json::json!({"n": i}),
372 )
373 .await
374 .expect("append");
375 ids.push(ev.event_id);
376 }
377 let other = port
378 .append(
379 "test.other",
380 None,
381 serde_json::json!({}),
382 serde_json::json!({"n": 99}),
383 )
384 .await
385 .expect("append other");
386
387 let page = port
388 .list_by_topic(topic, None, None, 3)
389 .await
390 .expect("list_by_topic");
391 assert_eq!(page.len(), 3);
392 assert_eq!(page[0].event_id, ids[0]);
393 assert_eq!(page[1].event_id, ids[1]);
394 assert_eq!(page[2].event_id, ids[2]);
395 assert!(page.windows(2).all(|w| w[0].seq < w[1].seq));
396
397 let after = port
398 .list_by_topic(topic, None, Some(2), 10)
399 .await
400 .expect("after_seq");
401 assert!(after.iter().all(|e| e.seq > 2));
402 assert!(after.iter().all(|e| e.topic_name == topic));
403
404 let recent = port.list_recent(2).await.expect("list_recent");
405 assert_eq!(recent.len(), 2);
406 assert!(recent[0].created_at >= recent[1].created_at);
407 assert!(recent
408 .iter()
409 .any(|e| e.event_id == other.event_id || e.topic_name == topic));
410
411 let empty = port
412 .list_by_topic(topic, None, None, 0)
413 .await
414 .expect("zero limit");
415 assert!(empty.is_empty());
416 }
417}