Skip to main content

tower_mcp/
event_store.rs

1//! Pluggable storage for SSE events enabling stream resumption.
2//!
3//! Mirrors the shape of [`crate::session_store`]: a trait, a serializable
4//! record type, an error enum, an in-memory default, and a caching wrapper.
5//!
6//! SSE streams buffer events per session so clients can replay them after a
7//! disconnect via the `Last-Event-ID` header (SEP-1699). When session
8//! metadata lives in an external store ([`crate::session_store`]), the event
9//! buffer needs to move along with it -- otherwise stream resumption only
10//! works if the client reconnects to the exact instance that saw the
11//! original events. An external [`EventStore`] fixes this.
12//!
13//! The store contains resumable notification events only. Legacy
14//! server-to-client requests are associated with an originating POST response
15//! stream and deliberately have no event IDs, persistence, or replay.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use std::sync::Arc;
21//! use tower_mcp::event_store::{EventStore, MemoryEventStore};
22//!
23//! let store: Arc<dyn EventStore> = Arc::new(MemoryEventStore::new());
24//! // `HttpTransport::new(router).event_store(store)` once wired in.
25//! ```
26
27use std::collections::{HashMap, VecDeque};
28use std::sync::Arc;
29use std::time::SystemTime;
30
31use async_trait::async_trait;
32use serde::{Deserialize, Serialize};
33use tokio::sync::RwLock;
34
35/// Default capacity of the in-memory per-session ring buffer.
36pub const DEFAULT_MAX_EVENTS_PER_SESSION: usize = 1000;
37
38/// Serializable SSE event record.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[non_exhaustive]
41pub struct EventRecord {
42    /// Monotonically increasing event ID within a session.
43    pub id: u64,
44    /// Serialized JSON-RPC payload (notification, response, or request).
45    pub data: String,
46    /// When the event was produced.
47    pub timestamp: SystemTime,
48}
49
50impl EventRecord {
51    /// Create a new record stamped with the current time.
52    pub fn new(id: u64, data: impl Into<String>) -> Self {
53        Self {
54            id,
55            data: data.into(),
56            timestamp: SystemTime::now(),
57        }
58    }
59}
60
61/// Errors returned by [`EventStore`] implementations.
62#[derive(Debug, thiserror::Error)]
63#[non_exhaustive]
64pub enum EventStoreError {
65    /// Failed to encode a record.
66    #[error("encode error: {0}")]
67    Encode(String),
68    /// Failed to decode a record.
69    #[error("decode error: {0}")]
70    Decode(String),
71    /// Backend error (e.g. connection failure, transient storage error).
72    #[error("backend error: {0}")]
73    Backend(String),
74}
75
76/// Result alias for event store operations.
77pub type Result<T> = std::result::Result<T, EventStoreError>;
78
79/// Storage backend for per-session SSE event logs.
80///
81/// Implementations persist [`EventRecord`]s keyed by session ID. The default
82/// implementation is [`MemoryEventStore`]; external stores (Redis, etc.)
83/// typically live in separate crates.
84#[async_trait]
85pub trait EventStore: Send + Sync + 'static {
86    /// Append an event to a session's log.
87    async fn append(&self, session_id: &str, event: EventRecord) -> Result<()>;
88
89    /// Return events with IDs strictly greater than `after_id`, in order.
90    async fn replay_after(&self, session_id: &str, after_id: u64) -> Result<Vec<EventRecord>>;
91
92    /// Remove all events for a session. Idempotent.
93    async fn purge_session(&self, session_id: &str) -> Result<()>;
94}
95
96/// In-memory [`EventStore`] with a per-session ring buffer.
97///
98/// Each session keeps up to `max_events_per_session` events; the oldest is
99/// evicted when the buffer is full. This is the default store; external
100/// implementations are only needed for cross-instance replay.
101#[derive(Debug, Clone)]
102pub struct MemoryEventStore {
103    inner: Arc<RwLock<HashMap<String, VecDeque<EventRecord>>>>,
104    max_events_per_session: usize,
105}
106
107impl Default for MemoryEventStore {
108    fn default() -> Self {
109        Self::with_capacity(DEFAULT_MAX_EVENTS_PER_SESSION)
110    }
111}
112
113impl MemoryEventStore {
114    /// Create a new store using [`DEFAULT_MAX_EVENTS_PER_SESSION`].
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Create a store with the given per-session buffer capacity.
120    pub fn with_capacity(max_events_per_session: usize) -> Self {
121        Self {
122            inner: Arc::new(RwLock::new(HashMap::new())),
123            max_events_per_session,
124        }
125    }
126
127    /// Total number of events currently buffered across all sessions.
128    pub async fn total_events(&self) -> usize {
129        self.inner.read().await.values().map(|v| v.len()).sum()
130    }
131
132    /// Number of sessions with at least one buffered event.
133    pub async fn session_count(&self) -> usize {
134        self.inner.read().await.len()
135    }
136}
137
138#[async_trait]
139impl EventStore for MemoryEventStore {
140    async fn append(&self, session_id: &str, event: EventRecord) -> Result<()> {
141        let mut map = self.inner.write().await;
142        let buf = map.entry(session_id.to_string()).or_default();
143        if buf.len() >= self.max_events_per_session {
144            buf.pop_front();
145        }
146        buf.push_back(event);
147        Ok(())
148    }
149
150    async fn replay_after(&self, session_id: &str, after_id: u64) -> Result<Vec<EventRecord>> {
151        let map = self.inner.read().await;
152        Ok(match map.get(session_id) {
153            Some(buf) => buf.iter().filter(|e| e.id > after_id).cloned().collect(),
154            None => Vec::new(),
155        })
156    }
157
158    async fn purge_session(&self, session_id: &str) -> Result<()> {
159        self.inner.write().await.remove(session_id);
160        Ok(())
161    }
162}
163
164/// Two-tier [`EventStore`] composed of a cache frontend and a store backend.
165///
166/// Writes go to both tiers; replays read from the cache first and fall
167/// through to the backend on miss (populating the cache with the missing
168/// events). Mirrors [`crate::session_store::CachingSessionStore`].
169#[derive(Debug, Clone)]
170pub struct CachingEventStore<Cache, Store> {
171    cache: Cache,
172    store: Store,
173}
174
175impl<Cache, Store> CachingEventStore<Cache, Store> {
176    /// Create a new caching store with the given cache and backend.
177    pub fn new(cache: Cache, store: Store) -> Self {
178        Self { cache, store }
179    }
180}
181
182#[async_trait]
183impl<Cache, Store> EventStore for CachingEventStore<Cache, Store>
184where
185    Cache: EventStore,
186    Store: EventStore,
187{
188    async fn append(&self, session_id: &str, event: EventRecord) -> Result<()> {
189        // Write the backend first so durability is established, then mirror.
190        self.store.append(session_id, event.clone()).await?;
191        self.cache.append(session_id, event).await?;
192        Ok(())
193    }
194
195    async fn replay_after(&self, session_id: &str, after_id: u64) -> Result<Vec<EventRecord>> {
196        let cached = self.cache.replay_after(session_id, after_id).await?;
197        if !cached.is_empty() {
198            return Ok(cached);
199        }
200        let from_store = self.store.replay_after(session_id, after_id).await?;
201        // Warm the cache best-effort; failures are non-fatal.
202        for event in &from_store {
203            let _ = self.cache.append(session_id, event.clone()).await;
204        }
205        Ok(from_store)
206    }
207
208    async fn purge_session(&self, session_id: &str) -> Result<()> {
209        let cache_result = self.cache.purge_session(session_id).await;
210        let store_result = self.store.purge_session(session_id).await;
211        cache_result.and(store_result)
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[tokio::test]
220    async fn memory_store_append_and_replay() {
221        let store = MemoryEventStore::new();
222        for i in 0..3 {
223            store
224                .append("s", EventRecord::new(i, format!("e{i}")))
225                .await
226                .unwrap();
227        }
228
229        let all = store.replay_after("s", 0).await.unwrap();
230        assert_eq!(all.len(), 2); // ids 1, 2
231
232        let none = store.replay_after("s", 5).await.unwrap();
233        assert!(none.is_empty());
234
235        let after_first = store.replay_after("s", 0).await.unwrap();
236        assert_eq!(after_first[0].id, 1);
237        assert_eq!(after_first[1].id, 2);
238    }
239
240    #[tokio::test]
241    async fn memory_store_respects_capacity() {
242        let store = MemoryEventStore::with_capacity(3);
243        for i in 0..5 {
244            store
245                .append("s", EventRecord::new(i, format!("e{i}")))
246                .await
247                .unwrap();
248        }
249
250        // Oldest two events evicted (ids 0 and 1), leaving 2, 3, 4.
251        let events = store.replay_after("s", 0).await.unwrap();
252        assert_eq!(events.len(), 3);
253        assert_eq!(events[0].id, 2);
254        assert_eq!(events[2].id, 4);
255    }
256
257    #[tokio::test]
258    async fn memory_store_isolates_sessions() {
259        let store = MemoryEventStore::new();
260        store.append("a", EventRecord::new(0, "a0")).await.unwrap();
261        store.append("b", EventRecord::new(0, "b0")).await.unwrap();
262
263        let a = store.replay_after("a", 0).await.unwrap();
264        let b = store.replay_after("b", 0).await.unwrap();
265        assert!(a.is_empty() && b.is_empty(), "after_id filters out id 0");
266
267        // Replay from just before 0 (using -1 isn't possible with u64, so
268        // we verify by appending a second event and replaying after 0).
269        store.append("a", EventRecord::new(1, "a1")).await.unwrap();
270        let a1 = store.replay_after("a", 0).await.unwrap();
271        assert_eq!(a1.len(), 1);
272        assert_eq!(a1[0].data, "a1");
273    }
274
275    #[tokio::test]
276    async fn memory_store_purge_removes_session() {
277        let store = MemoryEventStore::new();
278        store.append("s", EventRecord::new(0, "x")).await.unwrap();
279        assert_eq!(store.session_count().await, 1);
280
281        store.purge_session("s").await.unwrap();
282        assert_eq!(store.session_count().await, 0);
283    }
284
285    #[tokio::test]
286    async fn memory_store_purge_is_idempotent() {
287        MemoryEventStore::new()
288            .purge_session("nonexistent")
289            .await
290            .unwrap();
291    }
292
293    #[tokio::test]
294    async fn dyn_event_store_object_safe() {
295        let store: Arc<dyn EventStore> = Arc::new(MemoryEventStore::new());
296        store.append("s", EventRecord::new(0, "x")).await.unwrap();
297    }
298
299    #[tokio::test]
300    async fn caching_store_writes_to_both_tiers() {
301        let cache = MemoryEventStore::new();
302        let backend = MemoryEventStore::new();
303        let store = CachingEventStore::new(cache.clone(), backend.clone());
304
305        store.append("s", EventRecord::new(0, "x")).await.unwrap();
306
307        assert_eq!(cache.total_events().await, 1);
308        assert_eq!(backend.total_events().await, 1);
309    }
310
311    #[tokio::test]
312    async fn caching_store_reads_from_cache_first() {
313        let cache = MemoryEventStore::new();
314        let backend = MemoryEventStore::new();
315
316        // Only prime the backend.
317        backend
318            .append("s", EventRecord::new(0, "b0"))
319            .await
320            .unwrap();
321        backend
322            .append("s", EventRecord::new(1, "b1"))
323            .await
324            .unwrap();
325
326        let store = CachingEventStore::new(cache.clone(), backend);
327
328        // First replay goes to the backend and warms the cache.
329        let first = store.replay_after("s", 0).await.unwrap();
330        assert_eq!(first.len(), 1); // only id 1
331
332        // Cache should now contain the warmed event.
333        assert_eq!(cache.total_events().await, 1);
334    }
335
336    #[tokio::test]
337    async fn caching_store_purge_clears_both() {
338        let cache = MemoryEventStore::new();
339        let backend = MemoryEventStore::new();
340        let store = CachingEventStore::new(cache.clone(), backend.clone());
341
342        store.append("s", EventRecord::new(0, "x")).await.unwrap();
343        store.purge_session("s").await.unwrap();
344
345        assert_eq!(cache.total_events().await, 0);
346        assert_eq!(backend.total_events().await, 0);
347    }
348}