Skip to main content

photon_backend/storage/
in_proc.rs

1//! In-process storage port — broadcast bus, replay buffer, checkpoints (no external deps).
2
3use std::collections::{HashMap, VecDeque};
4use std::pin::Pin;
5use std::sync::Arc;
6
7use async_stream::stream;
8use async_trait::async_trait;
9use chrono::Utc;
10use dashmap::DashMap;
11use futures::stream::Stream;
12use serde_json::Value;
13use tokio::sync::{broadcast, RwLock};
14use uuid::Uuid;
15
16use super::partition::topic_filter_matches;
17use super::port::{StorageCapabilities, StoragePort};
18use crate::error::Result;
19use crate::event::TransportCrypto;
20use crate::models::Event;
21
22const DEFAULT_REPLAY_BUFFER_SIZE: usize = 1000;
23
24fn partition_key(topic_name: &str, topic_key: Option<&str>) -> String {
25    format!("{}:{}", topic_name, topic_key.unwrap_or("__null__"))
26}
27
28fn replay_key(topic_name: &str, topic_key_filter: Option<&str>) -> String {
29    partition_key(topic_name, topic_key_filter)
30}
31
32/// In-process storage for the `mem` adapter tier.
33pub struct InProcStoragePort {
34    crypto: TransportCrypto,
35    tx: broadcast::Sender<Event>,
36    replay_buffer: Arc<RwLock<HashMap<String, VecDeque<Event>>>>,
37    replay_buffer_size: usize,
38    events: Arc<DashMap<String, Event>>,
39    seq_counters: Arc<DashMap<String, i64>>,
40    checkpoints: Arc<DashMap<String, i64>>,
41    delivery_pins: Arc<DashMap<String, i64>>,
42}
43
44impl InProcStoragePort {
45    /// Create a new in-process port with default replay buffer size.
46    #[must_use]
47    pub fn new(crypto: TransportCrypto) -> Self {
48        let (tx, _) = broadcast::channel(1024);
49        Self {
50            crypto,
51            tx,
52            replay_buffer: Arc::new(RwLock::new(HashMap::new())),
53            replay_buffer_size: DEFAULT_REPLAY_BUFFER_SIZE,
54            events: Arc::new(DashMap::new()),
55            seq_counters: Arc::new(DashMap::new()),
56            checkpoints: Arc::new(DashMap::new()),
57            delivery_pins: Arc::new(DashMap::new()),
58        }
59    }
60
61    fn checkpoint_key(sub: &str, topic: &str, topic_key: Option<&str>) -> String {
62        format!("{sub}:{}:{}", topic, topic_key.unwrap_or("__null__"))
63    }
64
65    #[allow(clippy::significant_drop_tightening)]
66    async fn push_replay(&self, event: &Event) {
67        let key = replay_key(&event.topic_name, event.topic_key.as_deref());
68        let event = event.clone();
69        let mut buf = self.replay_buffer.write().await;
70        let queue = buf.entry(key).or_default();
71        queue.push_back(event);
72        while queue.len() > self.replay_buffer_size {
73            queue.pop_front();
74        }
75    }
76
77    fn next_seq(&self, topic_name: &str, topic_key: Option<&str>) -> i64 {
78        let key = partition_key(topic_name, topic_key);
79        let mut entry = self.seq_counters.entry(key).or_insert(0);
80        *entry += 1;
81        *entry
82    }
83}
84
85#[async_trait]
86impl StoragePort for InProcStoragePort {
87    fn capabilities(&self) -> StorageCapabilities {
88        StorageCapabilities::mem()
89    }
90
91    async fn append(
92        &self,
93        topic_name: &str,
94        topic_key: Option<&str>,
95        actor_json: Value,
96        payload_json: Value,
97    ) -> Result<Event> {
98        let _ = self.crypto.encrypt(&actor_json, &payload_json)?;
99        let seq = self.next_seq(topic_name, topic_key);
100        let event = Event {
101            event_id: Uuid::new_v4().to_string(),
102            topic_name: topic_name.to_string(),
103            topic_key: topic_key.map(String::from),
104            seq,
105            actor_json,
106            payload_json,
107            created_at: Utc::now(),
108        };
109        self.events
110            .insert(event.event_id.clone(), event.clone());
111        self.push_replay(&event).await;
112        let _ = self.tx.send(event.clone());
113        Ok(event)
114    }
115
116    fn subscribe(
117        &self,
118        topic_name: String,
119        topic_key_filter: Option<String>,
120        after_seq: Option<i64>,
121    ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
122        let replay_key = replay_key(&topic_name, topic_key_filter.as_deref());
123        let replay_buffer = Arc::clone(&self.replay_buffer);
124        let mut live_rx = self.tx.subscribe();
125        let topic = topic_name;
126        let filter = topic_key_filter;
127        let delivery_pins = Arc::clone(&self.delivery_pins);
128
129        Box::pin(stream! {
130            if let Some(seq) = after_seq {
131                let buf = replay_buffer.read().await;
132                if let Some(queue) = buf.get(&replay_key) {
133                    for evt in queue {
134                        if evt.seq > seq && topic_filter_matches(evt, &topic, filter.as_ref()) {
135                            yield Ok(evt.clone());
136                        }
137                    }
138                }
139            }
140
141            loop {
142                match live_rx.recv().await {
143                    Ok(ev) => {
144                        if !topic_filter_matches(&ev, &topic, filter.as_ref()) {
145                            continue;
146                        }
147                        if after_seq.is_some_and(|s| ev.seq <= s) {
148                            continue;
149                        }
150                        let pin_key = partition_key(&ev.topic_name, ev.topic_key.as_deref());
151                        delivery_pins.insert(pin_key, ev.seq);
152                        yield Ok(ev);
153                    }
154                    Err(broadcast::error::RecvError::Lagged(_)) => {
155                        // Live fanout dropped messages; catch up from in-memory replay buffer.
156                        let pin_key = partition_key(&topic, filter.as_deref());
157                        let after = delivery_pins
158                            .get(&pin_key)
159                            .map(|v| *v)
160                            .or(after_seq)
161                            .unwrap_or(0);
162                        let buf = replay_buffer.read().await;
163                        if let Some(queue) = buf.get(&replay_key) {
164                            for evt in queue {
165                                if evt.seq > after
166                                    && topic_filter_matches(evt, &topic, filter.as_ref())
167                                {
168                                    let pk = partition_key(
169                                        &evt.topic_name,
170                                        evt.topic_key.as_deref(),
171                                    );
172                                    delivery_pins.insert(pk, evt.seq);
173                                    yield Ok(evt.clone());
174                                }
175                            }
176                        }
177                    }
178                    Err(broadcast::error::RecvError::Closed) => break,
179                }
180            }
181        })
182    }
183
184    async fn get_event(&self, event_id: &str) -> Result<Option<Event>> {
185        Ok(self.events.get(event_id).map(|e| e.clone()))
186    }
187
188    async fn load_checkpoint(
189        &self,
190        subscription_name: &str,
191        topic_name: &str,
192        topic_key: Option<&str>,
193    ) -> Result<Option<i64>> {
194        let key = Self::checkpoint_key(subscription_name, topic_name, topic_key);
195        Ok(self.checkpoints.get(&key).map(|v| *v))
196    }
197
198    async fn commit_checkpoint(
199        &self,
200        subscription_name: &str,
201        topic_name: &str,
202        topic_key: Option<&str>,
203        last_seq: i64,
204    ) -> Result<()> {
205        let key = Self::checkpoint_key(subscription_name, topic_name, topic_key);
206        self.checkpoints.insert(key, last_seq);
207        Ok(())
208    }
209
210    async fn truncate_before(
211        &self,
212        topic_name: &str,
213        topic_key: Option<&str>,
214        truncate_bound: i64,
215    ) -> Result<u64> {
216        let key = replay_key(topic_name, topic_key);
217        let mut removed = 0u64;
218        let mut buf = self.replay_buffer.write().await;
219        if let Some(queue) = buf.get_mut(&key) {
220            while queue
221                .front()
222                .is_some_and(|e| e.seq < truncate_bound)
223            {
224                if let Some(ev) = queue.pop_front() {
225                    self.events.remove(&ev.event_id);
226                    removed += 1;
227                }
228            }
229        }
230        drop(buf);
231        Ok(removed)
232    }
233
234    async fn delivery_seq_pin(&self, topic_name: &str, topic_key: Option<&str>) -> Option<i64> {
235        let key = partition_key(topic_name, topic_key);
236        self.delivery_pins.get(&key).map(|v| *v)
237    }
238}