Skip to main content

mcpkit_server/
streams.rs

1//! Per-session, per-stream SSE delivery (shared by the HTTP adapters).
2//!
3//! The Streamable HTTP spec requires that the server *"MUST send each of its
4//! JSON-RPC messages on only one of the connected streams"*, that SSE event
5//! ids be *"globally unique across all streams within that session"* and
6//! *"SHOULD encode sufficient information to identify the originating
7//! stream"*, and that the server *"MUST NOT replay messages that would have
8//! been delivered on a different stream"*.
9//!
10//! [`StreamRegistry`] implements those rules once, so the four adapters share
11//! one delivery/replay implementation instead of four drifting copies:
12//!
13//! - each GET opens (or resumes) one stream with its own bounded `mpsc`
14//!   channel and its own replay buffer;
15//! - every outbound message is stored on, and delivered to, exactly **one**
16//!   stream — the *designated* stream (the oldest live one, stable while it
17//!   lives; a resumed stream keeps its identity and therefore its
18//!   designation);
19//! - event ids are `{stream_id}-{seq}`, allocated **once at store time**, so
20//!   the id on the wire always equals the id in the buffer and
21//!   `Last-Event-ID` replay works;
22//! - replay serves only events buffered on the stream the cursor names;
23//! - a full channel kills its stream (never a silent skip): the buffer is
24//!   retained for [`StreamConfig::max_age`], so a reconnecting client
25//!   resumes and replays what the dead channel missed.
26
27use std::collections::VecDeque;
28use std::sync::{Arc, Mutex};
29use std::time::{Duration, Instant};
30use tokio::sync::mpsc;
31
32/// An event stored for delivery and replay on one stream.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct StoredEvent {
35    /// Event id (`{stream_id}-{seq}`), allocated at store time.
36    pub id: String,
37    /// SSE event type (e.g. `connected`, `message`).
38    pub event_type: String,
39    /// Event payload.
40    pub data: String,
41}
42
43/// Configuration for a session's stream registry.
44#[derive(Debug, Clone)]
45#[non_exhaustive]
46pub struct StreamConfig {
47    /// Maximum buffered events retained per stream for replay.
48    pub max_events_per_stream: usize,
49    /// How long a dead stream's replay buffer is retained (spec resumability
50    /// window). Also bounds the age of buffered events on live streams.
51    pub max_age: Duration,
52    /// Per-stream delivery channel capacity. A stream whose channel is full
53    /// is killed explicitly (client resumes via `Last-Event-ID`).
54    pub channel_capacity: usize,
55}
56
57impl StreamConfig {
58    /// A stream configuration with default values.
59    #[must_use]
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Maximum buffered events retained per stream for replay.
65    #[must_use]
66    pub const fn max_events_per_stream(mut self, max: usize) -> Self {
67        self.max_events_per_stream = max;
68        self
69    }
70
71    /// How long a dead stream's replay buffer is retained.
72    #[must_use]
73    pub const fn max_age(mut self, max_age: Duration) -> Self {
74        self.max_age = max_age;
75        self
76    }
77
78    /// Per-stream delivery channel capacity.
79    #[must_use]
80    pub const fn channel_capacity(mut self, capacity: usize) -> Self {
81        self.channel_capacity = capacity;
82        self
83    }
84}
85
86impl Default for StreamConfig {
87    fn default() -> Self {
88        Self {
89            max_events_per_stream: 1000,
90            max_age: Duration::from_secs(300),
91            channel_capacity: 100,
92        }
93    }
94}
95
96#[derive(Debug)]
97struct StreamSlot {
98    id: u64,
99    /// Next sequence number to allocate on this stream.
100    seq: u64,
101    buffer: VecDeque<(Instant, StoredEvent)>,
102    /// Live delivery channel; `None` once the stream has died.
103    sender: Option<mpsc::Sender<StoredEvent>>,
104    opened: Instant,
105    died: Option<Instant>,
106}
107
108impl StreamSlot {
109    fn store(&mut self, event_type: &str, data: String, config: &StreamConfig) -> StoredEvent {
110        let event = StoredEvent {
111            id: format!("{}-{}", self.id, self.seq),
112            event_type: event_type.to_string(),
113            data,
114        };
115        self.seq += 1;
116        self.buffer.push_back((Instant::now(), event.clone()));
117        while self.buffer.len() > config.max_events_per_stream {
118            self.buffer.pop_front();
119        }
120        while self
121            .buffer
122            .front()
123            .is_some_and(|(at, _)| at.elapsed() > config.max_age)
124        {
125            self.buffer.pop_front();
126        }
127        event
128    }
129}
130
131/// Per-session registry of SSE streams. See the module docs for the rules it
132/// enforces.
133#[derive(Debug)]
134pub struct StreamRegistry {
135    inner: Mutex<Inner>,
136    config: StreamConfig,
137}
138
139#[derive(Debug)]
140struct Inner {
141    streams: Vec<StreamSlot>,
142    next_stream_id: u64,
143}
144
145/// One live stream: the receiving half consumed by the adapter's SSE loop.
146///
147/// Dropping the handle marks the stream dead in the registry (its replay
148/// buffer is retained for [`StreamConfig::max_age`]).
149#[derive(Debug)]
150pub struct StreamHandle {
151    stream_id: u64,
152    rx: mpsc::Receiver<StoredEvent>,
153    registry: Arc<StreamRegistry>,
154}
155
156impl StreamHandle {
157    /// This stream's id (the `{stream_id}` half of its event ids).
158    #[must_use]
159    pub const fn stream_id(&self) -> u64 {
160        self.stream_id
161    }
162
163    /// Receive the next event queued for this stream. `None` when the stream
164    /// has been killed (e.g. channel overflow) or the registry dropped.
165    pub async fn recv(&mut self) -> Option<StoredEvent> {
166        self.rx.recv().await
167    }
168}
169
170impl Drop for StreamHandle {
171    fn drop(&mut self) {
172        self.registry.mark_dead(self.stream_id);
173    }
174}
175
176impl StreamRegistry {
177    /// Create a registry with the given configuration.
178    #[must_use]
179    pub fn new(config: StreamConfig) -> Self {
180        Self {
181            inner: Mutex::new(Inner {
182                streams: Vec::new(),
183                next_stream_id: 1,
184            }),
185            config,
186        }
187    }
188
189    /// Open a new stream, storing and queueing a priming event (spec: the
190    /// server SHOULD immediately send an event with an id so the client can
191    /// reconnect with `Last-Event-ID`). Returns the handle and the priming
192    /// event.
193    pub fn open(
194        self: &Arc<Self>,
195        prime_event_type: &str,
196        prime_data: String,
197    ) -> (StreamHandle, StoredEvent) {
198        let (tx, rx) = mpsc::channel(self.config.channel_capacity);
199        let mut inner = self.inner.lock().expect("stream registry lock");
200        Self::reap(&mut inner, &self.config);
201        let id = inner.next_stream_id;
202        inner.next_stream_id += 1;
203        let mut slot = StreamSlot {
204            id,
205            seq: 0,
206            buffer: VecDeque::new(),
207            sender: Some(tx),
208            opened: Instant::now(),
209            died: None,
210        };
211        let prime = slot.store(prime_event_type, prime_data, &self.config);
212        inner.streams.push(slot);
213        drop(inner);
214        (
215            StreamHandle {
216                stream_id: id,
217                rx,
218                registry: Arc::clone(self),
219            },
220            prime,
221        )
222    }
223
224    /// Resume the stream named by `last_event_id` (`{stream_id}-{seq}`),
225    /// returning a fresh handle for the SAME stream identity plus the
226    /// buffered events after the cursor. `None` if the id does not parse, the
227    /// stream is unknown, or its buffer has been reaped.
228    ///
229    /// A resumed stream keeps its id — and therefore its designation if it
230    /// was the designated stream.
231    pub fn resume(
232        self: &Arc<Self>,
233        last_event_id: &str,
234    ) -> Option<(StreamHandle, Vec<StoredEvent>)> {
235        let (stream_id, seq) = parse_event_id(last_event_id)?;
236        let (tx, rx) = mpsc::channel(self.config.channel_capacity);
237        let mut inner = self.inner.lock().expect("stream registry lock");
238        Self::reap(&mut inner, &self.config);
239        let slot = inner.streams.iter_mut().find(|s| s.id == stream_id)?;
240        slot.sender = Some(tx);
241        slot.died = None;
242        let replay = slot
243            .buffer
244            .iter()
245            .filter(|(_, e)| parse_event_id(&e.id).is_some_and(|(_, s)| s > seq))
246            .map(|(_, e)| e.clone())
247            .collect();
248        drop(inner);
249        Some((
250            StreamHandle {
251                stream_id,
252                rx,
253                registry: Arc::clone(self),
254            },
255            replay,
256        ))
257    }
258
259    /// Store `data` on the designated stream (the oldest live one) and queue
260    /// it for delivery, returning the allocated event id. `None` when the
261    /// session has no live stream.
262    ///
263    /// A full channel kills the stream (explicitly, never a silent skip); the
264    /// event stays in that stream's buffer, so the client's resumption GET
265    /// replays it. The event is NOT re-sent on another stream — it belongs to
266    /// the stream it was stored on (spec: no cross-stream replay).
267    #[must_use]
268    pub fn send(&self, event_type: &str, data: String) -> Option<String> {
269        let mut inner = self.inner.lock().expect("stream registry lock");
270        Self::reap(&mut inner, &self.config);
271        let config = &self.config;
272        let slot = inner
273            .streams
274            .iter_mut()
275            .filter(|s| s.sender.is_some())
276            .min_by_key(|s| s.opened)?;
277        let event = slot.store(event_type, data, config);
278        if let Some(sender) = &slot.sender {
279            match sender.try_send(event.clone()) {
280                Ok(()) => {}
281                Err(mpsc::error::TrySendError::Full(_) | mpsc::error::TrySendError::Closed(_)) => {
282                    // Kill the stream; the client resumes and replays from
283                    // the retained buffer.
284                    slot.sender = None;
285                    slot.died = Some(Instant::now());
286                }
287            }
288        }
289        Some(event.id)
290    }
291
292    /// Whether any stream is currently live.
293    #[must_use]
294    pub fn has_live_stream(&self) -> bool {
295        self.inner
296            .lock()
297            .expect("stream registry lock")
298            .streams
299            .iter()
300            .any(|s| s.sender.is_some())
301    }
302
303    fn mark_dead(&self, stream_id: u64) {
304        if let Ok(mut inner) = self.inner.lock() {
305            if let Some(slot) = inner.streams.iter_mut().find(|s| s.id == stream_id) {
306                slot.sender = None;
307                slot.died = Some(Instant::now());
308            }
309        }
310    }
311
312    /// Drop dead streams whose retention window has passed.
313    fn reap(inner: &mut Inner, config: &StreamConfig) {
314        inner.streams.retain(|s| {
315            s.sender.is_some() || s.died.is_none_or(|at| at.elapsed() < config.max_age)
316        });
317    }
318}
319
320/// Parse a `{stream_id}-{seq}` event id.
321fn parse_event_id(id: &str) -> Option<(u64, u64)> {
322    let (stream, seq) = id.split_once('-')?;
323    Some((stream.parse().ok()?, seq.parse().ok()?))
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn registry() -> Arc<StreamRegistry> {
331        Arc::new(StreamRegistry::new(StreamConfig::default()))
332    }
333
334    #[tokio::test]
335    async fn send_delivers_to_exactly_one_stream() {
336        let reg = registry();
337        let (mut a, _) = reg.open("connected", "sid".into());
338        let (mut b, _) = reg.open("connected", "sid".into());
339
340        let id = reg.send("message", "hello".into()).expect("live stream");
341        // Designated = oldest live = stream a.
342        let got = a.recv().await.expect("delivered");
343        assert_eq!(got.id, id);
344        assert_eq!(got.data, "hello");
345        // Stream b must NOT receive it (spec MUST-NOT broadcast).
346        assert!(
347            tokio::time::timeout(Duration::from_millis(50), b.recv())
348                .await
349                .is_err(),
350            "second stream must not receive the message"
351        );
352    }
353
354    #[tokio::test]
355    async fn event_ids_encode_stream_and_sequence() {
356        let reg = registry();
357        let (_a, prime) = reg.open("connected", "sid".into());
358        assert_eq!(prime.id, "1-0");
359        let id1 = reg.send("message", "x".into()).unwrap();
360        let id2 = reg.send("message", "y".into()).unwrap();
361        assert_eq!(id1, "1-1");
362        assert_eq!(id2, "1-2");
363    }
364
365    #[tokio::test]
366    async fn resume_replays_only_same_stream_events_after_cursor() {
367        let reg = registry();
368        let (a, _) = reg.open("connected", "sid".into());
369        let id1 = reg.send("message", "one".into()).unwrap();
370        let _id2 = reg.send("message", "two".into()).unwrap();
371        drop(a); // stream dies
372
373        // A different stream's traffic must not appear in stream 1's replay.
374        let (_b, _) = reg.open("connected", "sid".into());
375        let _ = reg.send("message", "other-stream".into()).unwrap();
376
377        let (_a2, replay) = reg.resume(&id1).expect("resumable");
378        assert_eq!(replay.len(), 1, "only events after the cursor: {replay:?}");
379        assert_eq!(replay[0].data, "two");
380    }
381
382    #[tokio::test]
383    async fn resumed_stream_keeps_designation() {
384        let reg = registry();
385        let (a, prime) = reg.open("connected", "sid".into());
386        let (_b, _) = reg.open("connected", "sid".into());
387        drop(a);
388
389        // Stream 1 resumes; as the oldest it is designated again.
390        let (mut a2, _) = reg.resume(&prime.id).expect("resumable");
391        let id = reg.send("message", "after-resume".into()).unwrap();
392        assert!(
393            id.starts_with("1-"),
394            "designated must still be stream 1: {id}"
395        );
396        assert_eq!(a2.recv().await.unwrap().data, "after-resume");
397    }
398
399    #[tokio::test]
400    async fn overflow_kills_stream_and_replay_recovers() {
401        let reg = Arc::new(StreamRegistry::new(StreamConfig {
402            channel_capacity: 2,
403            ..StreamConfig::default()
404        }));
405        let (a, prime) = reg.open("connected", "sid".into());
406
407        // Fill the channel (capacity 2) without a reader, then overflow.
408        let _ = reg.send("message", "m1".into()).unwrap();
409        let _ = reg.send("message", "m2".into()).unwrap();
410        let id3 = reg.send("message", "m3".into()).unwrap();
411        assert!(!reg.has_live_stream(), "overflow must kill the stream");
412
413        // The overflowed event is in the buffer: resume from the prime id
414        // replays everything, including m3.
415        drop(a);
416        let (_a2, replay) = reg.resume(&prime.id).expect("resumable");
417        assert_eq!(replay.last().map(|e| e.id.as_str()), Some(id3.as_str()));
418        assert_eq!(replay.len(), 3);
419    }
420
421    #[tokio::test]
422    async fn no_live_stream_returns_none() {
423        let reg = registry();
424        assert!(reg.send("message", "x".into()).is_none());
425        let (a, _) = reg.open("connected", "sid".into());
426        drop(a);
427        assert!(
428            reg.send("message", "x".into()).is_none(),
429            "a dead stream is not a delivery target"
430        );
431    }
432
433    #[tokio::test]
434    async fn dead_stream_buffer_is_reaped_after_max_age() {
435        let reg = Arc::new(StreamRegistry::new(StreamConfig {
436            max_age: Duration::from_millis(10),
437            ..StreamConfig::default()
438        }));
439        let (a, prime) = reg.open("connected", "sid".into());
440        drop(a);
441        tokio::time::sleep(Duration::from_millis(30)).await;
442        // Access triggers the reap; the stream identity is gone.
443        let _ = reg.send("message", "x".into());
444        assert!(
445            reg.resume(&prime.id).is_none(),
446            "expired dead stream must not be resumable"
447        );
448    }
449
450    #[tokio::test]
451    async fn handle_drop_marks_stream_dead() {
452        let reg = registry();
453        let (a, _) = reg.open("connected", "sid".into());
454        assert!(reg.has_live_stream());
455        drop(a);
456        assert!(!reg.has_live_stream());
457    }
458}