Skip to main content

yeti_types/resource/
realtime.rs

1//! Real-time primitives: `Subscription`, `SubscriptionMessage`, `Connection`,
2//! and the `StaticFiles` abstraction.
3
4use futures::stream::Stream;
5use std::pin::Pin;
6
7use crate::error::{Result, YetiError};
8
9// ============================================================================
10// Real-time types
11// ============================================================================
12
13/// Subscription handle for real-time updates.
14pub struct Subscription {
15    /// Stream of real-time updates
16    pub stream: Pin<Box<dyn Stream<Item = Result<SubscriptionMessage>> + Send>>,
17    /// Initial queued messages (retained record, historical messages)
18    pub queue: Vec<SubscriptionMessage>,
19}
20
21impl std::fmt::Debug for Subscription {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("Subscription")
24            .field("queue", &self.queue)
25            // `stream` (dyn Stream) elided
26            .finish_non_exhaustive()
27    }
28}
29
30/// Message received from a subscription.
31#[derive(Debug, Clone)]
32pub struct SubscriptionMessage {
33    /// Type of message
34    pub message_type: MessageType,
35    /// Message payload
36    pub data: serde_json::Value,
37    /// Record/topic ID this message relates to
38    pub id: Option<String>,
39    /// Timestamp of the message
40    pub timestamp: chrono::DateTime<chrono::Utc>,
41}
42
43/// Type of subscription message.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum MessageType {
46    /// Record was updated (PUT, PATCH, POST)
47    Update,
48    /// Record was deleted
49    Delete,
50    /// Custom message published
51    Publish,
52    /// Initial/retained record sent on subscription start
53    Retained,
54}
55
56/// WebSocket/SSE connection handle.
57pub struct Connection {
58    /// Outgoing message stream (server -> client)
59    pub outgoing: Pin<Box<dyn Stream<Item = Result<serde_json::Value>> + Send>>,
60    /// Incoming message sink (client -> server, WebSocket only; None for SSE)
61    pub incoming: Option<Pin<Box<dyn futures::Sink<serde_json::Value, Error = YetiError> + Send>>>,
62}
63
64impl std::fmt::Debug for Connection {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("Connection")
67            .field("has_incoming", &self.incoming.is_some())
68            // `outgoing` (dyn Stream), `incoming` (dyn Sink) elided
69            .finish_non_exhaustive()
70    }
71}
72
73// ============================================================================
74// StaticFiles trait — abstraction for static file serving
75// ============================================================================
76
77/// Trait for serving static files (filesystem or embedded).
78///
79/// Implemented by `StaticFileHandler` and `EmbeddedStaticFileHandler`.
80/// The router stores `Option<Arc<dyn StaticFiles>>` to serve files
81/// without knowing the concrete handler type.
82pub trait StaticFiles: Send + Sync {
83    /// Whether this handler serves a Single-Page Application (SPA).
84    fn is_spa(&self) -> bool {
85        false
86    }
87    /// Serve the index page (for SPA fallback).
88    fn serve_index(&self, headers: &http::HeaderMap) -> Option<http::Response<bytes::Bytes>>;
89    /// Check if this handler can serve the given path.
90    fn matches(&self, path: &str) -> bool;
91    /// Serve a file at the given path (no SPA fallback).
92    fn serve_file_only(
93        &self,
94        path: &str,
95        headers: &http::HeaderMap,
96    ) -> Option<http::Response<bytes::Bytes>>;
97    /// Serve the configured custom 404 page, if any. Distinct from
98    /// [`Self::serve_index`] (the SPA fallback): this returns a 404
99    /// status with the user-authored not-found body (typically from
100    /// `[package.metadata.app.static].not_found`). Returns `None` when
101    /// no custom 404 is configured — the router falls back to its
102    /// platform JSON 404 in that case.
103    fn serve_not_found(&self, _headers: &http::HeaderMap) -> Option<http::Response<bytes::Bytes>> {
104        None
105    }
106}