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}