Skip to main content

projectx_client/realtime/
event_flow.rs

1// SPDX-FileCopyrightText: 2026 Kevin Monaghan
2// SPDX-License-Identifier: MIT-0
3
4//! Lossless data delivery with retained continuity and lifecycle boundaries.
5
6use std::{collections::VecDeque, time::Duration};
7
8use super::{
9    Arc, AtomicBool, AtomicUsize, Instant, Notify, Ordering, ParkingMutex, RealtimeError,
10    RealtimeEvent, fmt, mpsc,
11};
12
13/// Identity of one ready socket, scoped to its owning real-time client.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct RealtimeGeneration(pub(super) u64);
16
17/// An observation attributed to the exact socket that produced it.
18#[derive(Clone, Debug, PartialEq)]
19#[non_exhaustive]
20pub struct RealtimeMessage {
21    /// The originating socket's identity.
22    pub generation: RealtimeGeneration,
23    /// The observation or lifecycle boundary.
24    pub event: RealtimeEvent,
25}
26
27pub(super) struct EventFlow {
28    pub(super) state: ParkingMutex<EventFlowState>,
29    pub(super) queued_weight: AtomicUsize,
30    gap_pending: AtomicBool,
31    changed: Notify,
32}
33
34#[derive(Default)]
35pub(super) struct EventFlowState {
36    pub(super) active_generation: Option<u64>,
37    queued_at: VecDeque<Instant>,
38    gap: Option<GapFence>,
39}
40
41// Two retained lifecycle slots suffice: a generation with a real transport gap
42// may start and end, but another generation cannot start until it is consumed.
43struct GapFence {
44    generation: u64,
45    start_event: Option<RealtimeEvent>,
46    terminal_event: Option<RealtimeEvent>,
47    generation_ended: bool,
48    acknowledged: bool,
49}
50
51struct EventReservation {
52    flow: Arc<EventFlow>,
53    weight: usize,
54}
55
56impl Drop for EventReservation {
57    fn drop(&mut self) {
58        let previous = self
59            .flow
60            .queued_weight
61            .fetch_sub(self.weight, Ordering::AcqRel);
62        debug_assert!(previous >= self.weight);
63    }
64}
65
66pub(super) struct EventEnvelope {
67    message: RealtimeMessage,
68    reservation: EventReservation,
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub(super) enum PublishOutcome {
73    Published,
74    GapFenced,
75    StaleGeneration,
76}
77
78impl EventEnvelope {
79    fn into_message(self) -> RealtimeMessage {
80        let Self {
81            message,
82            reservation,
83        } = self;
84        drop(reservation);
85        message
86    }
87}
88
89impl EventFlow {
90    pub(super) fn new() -> Arc<Self> {
91        Arc::new(Self {
92            state: ParkingMutex::new(EventFlowState::default()),
93            queued_weight: AtomicUsize::new(0),
94            gap_pending: AtomicBool::new(false),
95            changed: Notify::new(),
96        })
97    }
98
99    pub(super) fn has_unacknowledged_gap(&self) -> bool {
100        self.gap_pending.load(Ordering::Acquire)
101    }
102
103    pub(super) fn start_generation(&self, generation: u64) -> Result<(), RealtimeError> {
104        let mut state = self.state.lock();
105        if state.gap.is_some() {
106            return Err(RealtimeError::TransportGapPending);
107        }
108        if state.active_generation.is_some() {
109            return Err(RealtimeError::ConnectionCancelled);
110        }
111        state.active_generation = Some(generation);
112        Ok(())
113    }
114
115    pub(super) fn publish(
116        self: &Arc<Self>,
117        events: &mpsc::UnboundedSender<EventEnvelope>,
118        generation: u64,
119        event: RealtimeEvent,
120        weight: usize,
121    ) -> Result<PublishOutcome, RealtimeError> {
122        let mut state = self.state.lock();
123        if state.active_generation != Some(generation) {
124            return Ok(PublishOutcome::StaleGeneration);
125        }
126        if state.gap.is_some() {
127            return Ok(self.retain_gap_boundary(&mut state, generation, event));
128        }
129        self.queued_weight.fetch_add(weight, Ordering::AcqRel);
130        let envelope = EventEnvelope {
131            message: RealtimeMessage {
132                generation: RealtimeGeneration(generation),
133                event,
134            },
135            reservation: EventReservation {
136                flow: Arc::clone(self),
137                weight,
138            },
139        };
140        match events.send(envelope) {
141            Ok(()) => {
142                state.queued_at.push_back(Instant::now());
143                Ok(PublishOutcome::Published)
144            }
145            Err(_) => Err(RealtimeError::EventReceiverClosed),
146        }
147    }
148
149    fn retain_gap_boundary(
150        &self,
151        state: &mut EventFlowState,
152        generation: u64,
153        event: RealtimeEvent,
154    ) -> PublishOutcome {
155        let fence = state.gap.get_or_insert(GapFence {
156            generation,
157            start_event: None,
158            terminal_event: None,
159            generation_ended: false,
160            acknowledged: false,
161        });
162        let retained = match event {
163            RealtimeEvent::Connected | RealtimeEvent::Reconnected => {
164                fence.start_event = Some(event);
165                true
166            }
167            RealtimeEvent::Disconnected => {
168                fence.terminal_event = Some(event);
169                true
170            }
171            _ => false,
172        };
173        self.gap_pending.store(true, Ordering::Release);
174        self.changed.notify_waiters();
175        if retained {
176            PublishOutcome::Published
177        } else {
178            PublishOutcome::GapFenced
179        }
180    }
181
182    pub(super) fn mark_gap(&self, generation: u64) {
183        let mut state = self.state.lock();
184        if state.active_generation == Some(generation) {
185            let _ = self.retain_gap_boundary(&mut state, generation, RealtimeEvent::TransportGap);
186        }
187    }
188
189    pub(super) fn finish_generation(&self, generation: u64) {
190        let mut state = self.state.lock();
191        if state.active_generation != Some(generation) {
192            return;
193        }
194        state.active_generation = None;
195        if let Some(fence) = state.gap.as_mut() {
196            fence.generation_ended = true;
197        }
198        drop(state);
199        self.changed.notify_waiters();
200    }
201
202    fn release_acknowledged(&self, state: &mut EventFlowState) {
203        if state.gap.as_ref().is_some_and(|fence| {
204            fence.acknowledged && fence.start_event.is_none() && fence.terminal_event.is_none()
205        }) {
206            state.gap = None;
207            self.gap_pending.store(false, Ordering::Release);
208        }
209    }
210
211    fn acknowledge_gap(&self) {
212        let mut state = self.state.lock();
213        if let Some(fence) = state.gap.as_mut() {
214            fence.acknowledged = true;
215        }
216        self.release_acknowledged(&mut state);
217        drop(state);
218        self.changed.notify_waiters();
219    }
220}
221
222/// Single-consumer receiver for real-time events.
223pub struct RealtimeEventReceiver {
224    pub(super) events: mpsc::UnboundedReceiver<EventEnvelope>,
225    pub(super) flow: Arc<EventFlow>,
226    pub(super) gap_reported: bool,
227}
228
229impl RealtimeEventReceiver {
230    /// Number of accepted events waiting for this receiver.
231    #[must_use]
232    pub fn queued_event_count(&self) -> usize {
233        self.flow.state.lock().queued_at.len()
234    }
235
236    /// Age of the oldest accepted event waiting for this receiver.
237    #[must_use]
238    pub fn oldest_event_age(&self) -> Option<Duration> {
239        self.flow
240            .state
241            .lock()
242            .queued_at
243            .front()
244            .map(Instant::elapsed)
245    }
246
247    /// Returns whether every producer for this event stream is gone.
248    #[must_use]
249    pub fn is_closed(&self) -> bool {
250        self.events.is_closed()
251    }
252
253    /// Receives an event without its socket identity.
254    /// Use [`Self::recv_message`] when work may overlap transport replacement.
255    pub async fn recv(&mut self) -> Option<RealtimeEvent> {
256        self.recv_message().await.map(|message| message.event)
257    }
258
259    /// Receives the accepted prefix, then retained gap and lifecycle boundaries.
260    /// A disconnected boundary proves both socket tasks have stopped.
261    pub async fn recv_message(&mut self) -> Option<RealtimeMessage> {
262        loop {
263            let changed = self.flow.changed.notified();
264            tokio::pin!(changed);
265            let _ = changed.as_mut().enable();
266            {
267                let mut state = self.flow.state.lock();
268                if let Ok(envelope) = self.events.try_recv() {
269                    state.queued_at.pop_front();
270                    return Some(envelope.into_message());
271                }
272                if let Some(fence) = state.gap.as_mut() {
273                    let event = if let Some(event) = fence.start_event.take() {
274                        Some(event)
275                    } else if !fence.acknowledged && !self.gap_reported {
276                        self.gap_reported = true;
277                        Some(RealtimeEvent::TransportGap)
278                    } else if fence.generation_ended {
279                        fence.terminal_event.take()
280                    } else {
281                        None
282                    };
283                    if let Some(event) = event {
284                        let message = RealtimeMessage {
285                            generation: RealtimeGeneration(fence.generation),
286                            event,
287                        };
288                        self.flow.release_acknowledged(&mut state);
289                        return Some(message);
290                    }
291                }
292                if self.events.is_closed() {
293                    return None;
294                }
295            }
296            tokio::select! {
297                biased;
298                envelope = self.events.recv() => {
299                    if let Some(envelope) = envelope {
300                        self.flow.state.lock().queued_at.pop_front();
301                        return Some(envelope.into_message());
302                    }
303                }
304                () = &mut changed => {}
305            }
306        }
307    }
308
309    /// Resumes data admission after the caller installs its recovery boundary.
310    /// This never closes a socket. Completions and keepalives continue while fenced.
311    pub fn acknowledge_transport_gap(&mut self) {
312        if self.gap_reported {
313            self.flow.acknowledge_gap();
314            self.gap_reported = false;
315        }
316    }
317}
318
319impl Drop for RealtimeEventReceiver {
320    fn drop(&mut self) {
321        let mut state = self.flow.state.lock();
322        self.events.close();
323        state.queued_at.clear();
324    }
325}
326
327impl fmt::Debug for RealtimeEventReceiver {
328    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
329        formatter
330            .debug_struct("RealtimeEventReceiver")
331            .field("gap_pending", &self.flow.has_unacknowledged_gap())
332            .field("queued_events", &self.queued_event_count())
333            .field("oldest_event_age", &self.oldest_event_age())
334            .field(
335                "queued_weight",
336                &self.flow.queued_weight.load(Ordering::Acquire),
337            )
338            .field("gap_reported", &self.gap_reported)
339            .finish_non_exhaustive()
340    }
341}