hyperstack_server/websocket/
frame.rs

1use serde::{Deserialize, Serialize};
2
3/// Streaming mode for different data access patterns
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
5#[serde(rename_all = "lowercase")]
6pub enum Mode {
7    /// Latest value only (watch semantics)
8    State,
9    /// Append-only stream
10    Append,
11    /// Collection/list view (also used for key-value lookups)
12    List,
13}
14
15/// Sort order for sorted views
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "lowercase")]
18pub enum SortOrder {
19    Asc,
20    Desc,
21}
22
23/// Sort configuration for a view
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct SortConfig {
26    /// Field path to sort by (e.g., ["id", "roundId"])
27    pub field: Vec<String>,
28    /// Sort order
29    pub order: SortOrder,
30}
31
32/// Subscription acknowledgment frame sent when a client subscribes
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SubscribedFrame {
35    /// Operation type - always "subscribed"
36    pub op: &'static str,
37    /// The view that was subscribed to
38    pub view: String,
39    /// Streaming mode for this view
40    pub mode: Mode,
41    /// Sort configuration if this is a sorted view
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub sort: Option<SortConfig>,
44}
45
46impl SubscribedFrame {
47    pub fn new(view: String, mode: Mode, sort: Option<SortConfig>) -> Self {
48        Self {
49            op: "subscribed",
50            view,
51            mode,
52            sort,
53        }
54    }
55}
56
57/// Data frame sent over WebSocket
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Frame {
60    pub mode: Mode,
61    #[serde(rename = "entity")]
62    pub export: String,
63    pub op: &'static str,
64    pub key: String,
65    pub data: serde_json::Value,
66    #[serde(skip_serializing_if = "Vec::is_empty", default)]
67    pub append: Vec<String>,
68}
69
70/// A single entity within a snapshot
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct SnapshotEntity {
73    pub key: String,
74    pub data: serde_json::Value,
75}
76
77/// Batch snapshot frame for initial data load
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct SnapshotFrame {
80    pub mode: Mode,
81    #[serde(rename = "entity")]
82    pub export: String,
83    pub op: &'static str,
84    pub data: Vec<SnapshotEntity>,
85    /// Indicates whether this is the final snapshot batch.
86    /// When `false`, more snapshot batches will follow.
87    /// When `true`, the snapshot is complete and live streaming begins.
88    #[serde(default = "default_complete")]
89    pub complete: bool,
90}
91
92fn default_complete() -> bool {
93    true
94}
95
96impl Frame {
97    pub fn entity(&self) -> &str {
98        &self.export
99    }
100
101    pub fn key(&self) -> &str {
102        &self.key
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_frame_entity_key_accessors() {
112        let frame = Frame {
113            mode: Mode::List,
114            export: "SettlementGame/list".to_string(),
115            op: "upsert",
116            key: "123".to_string(),
117            data: serde_json::json!({}),
118            append: vec![],
119        };
120
121        assert_eq!(frame.entity(), "SettlementGame/list");
122        assert_eq!(frame.key(), "123");
123    }
124
125    #[test]
126    fn test_frame_serialization() {
127        let frame = Frame {
128            mode: Mode::List,
129            export: "SettlementGame/list".to_string(),
130            op: "upsert",
131            key: "123".to_string(),
132            data: serde_json::json!({"gameId": "123"}),
133            append: vec![],
134        };
135
136        let json = serde_json::to_value(&frame).unwrap();
137        assert_eq!(json["op"], "upsert");
138        assert_eq!(json["mode"], "list");
139        assert_eq!(json["entity"], "SettlementGame/list");
140        assert_eq!(json["key"], "123");
141    }
142
143    #[test]
144    fn test_snapshot_frame_complete_serialization() {
145        let frame = SnapshotFrame {
146            mode: Mode::List,
147            export: "tokens/list".to_string(),
148            op: "snapshot",
149            data: vec![SnapshotEntity {
150                key: "abc".to_string(),
151                data: serde_json::json!({"id": "abc"}),
152            }],
153            complete: false,
154        };
155
156        let json = serde_json::to_value(&frame).unwrap();
157        assert_eq!(json["complete"], false);
158        assert_eq!(json["op"], "snapshot");
159    }
160
161    #[test]
162    fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
163        #[derive(Debug, Deserialize)]
164        struct TestSnapshotFrame {
165            #[allow(dead_code)]
166            mode: Mode,
167            #[allow(dead_code)]
168            #[serde(rename = "entity")]
169            export: String,
170            #[allow(dead_code)]
171            op: String,
172            #[allow(dead_code)]
173            data: Vec<SnapshotEntity>,
174            #[serde(default = "super::default_complete")]
175            complete: bool,
176        }
177
178        let json_without_complete = serde_json::json!({
179            "mode": "list",
180            "entity": "tokens/list",
181            "op": "snapshot",
182            "data": []
183        });
184
185        let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
186        assert!(frame.complete);
187    }
188
189    #[test]
190    fn test_snapshot_frame_batching_fields() {
191        let first_batch = SnapshotFrame {
192            mode: Mode::List,
193            export: "tokens/list".to_string(),
194            op: "snapshot",
195            data: vec![],
196            complete: false,
197        };
198
199        let final_batch = SnapshotFrame {
200            mode: Mode::List,
201            export: "tokens/list".to_string(),
202            op: "snapshot",
203            data: vec![],
204            complete: true,
205        };
206
207        assert!(!first_batch.complete);
208        assert!(final_batch.complete);
209    }
210}