Skip to main content

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
96/// Transform large u64 values to strings for JavaScript compatibility.
97/// JavaScript's Number.MAX_SAFE_INTEGER is 2^53 - 1 (9007199254740991).
98/// Values larger than this will lose precision in JavaScript.
99pub fn transform_large_u64_to_strings(value: &mut serde_json::Value) {
100    const MAX_SAFE_INTEGER: u64 = 9007199254740991; // 2^53 - 1
101
102    match value {
103        serde_json::Value::Object(map) => {
104            for (_, v) in map.iter_mut() {
105                transform_large_u64_to_strings(v);
106            }
107        }
108        serde_json::Value::Array(arr) => {
109            for v in arr.iter_mut() {
110                transform_large_u64_to_strings(v);
111            }
112        }
113        serde_json::Value::Number(n) => {
114            if let Some(n_u64) = n.as_u64() {
115                if n_u64 > MAX_SAFE_INTEGER {
116                    *value = serde_json::Value::String(n_u64.to_string());
117                }
118            } else if let Some(n_i64) = n.as_i64() {
119                const MIN_SAFE_INTEGER: i64 = -(MAX_SAFE_INTEGER as i64);
120                if n_i64 < MIN_SAFE_INTEGER {
121                    *value = serde_json::Value::String(n_i64.to_string());
122                }
123            }
124        }
125        _ => {}
126    }
127}
128
129impl Frame {
130    pub fn entity(&self) -> &str {
131        &self.export
132    }
133
134    pub fn key(&self) -> &str {
135        &self.key
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_frame_entity_key_accessors() {
145        let frame = Frame {
146            mode: Mode::List,
147            export: "SettlementGame/list".to_string(),
148            op: "upsert",
149            key: "123".to_string(),
150            data: serde_json::json!({}),
151            append: vec![],
152        };
153
154        assert_eq!(frame.entity(), "SettlementGame/list");
155        assert_eq!(frame.key(), "123");
156    }
157
158    #[test]
159    fn test_frame_serialization() {
160        let frame = Frame {
161            mode: Mode::List,
162            export: "SettlementGame/list".to_string(),
163            op: "upsert",
164            key: "123".to_string(),
165            data: serde_json::json!({"gameId": "123"}),
166            append: vec![],
167        };
168
169        let json = serde_json::to_value(&frame).unwrap();
170        assert_eq!(json["op"], "upsert");
171        assert_eq!(json["mode"], "list");
172        assert_eq!(json["entity"], "SettlementGame/list");
173        assert_eq!(json["key"], "123");
174    }
175
176    #[test]
177    fn test_snapshot_frame_complete_serialization() {
178        let frame = SnapshotFrame {
179            mode: Mode::List,
180            export: "tokens/list".to_string(),
181            op: "snapshot",
182            data: vec![SnapshotEntity {
183                key: "abc".to_string(),
184                data: serde_json::json!({"id": "abc"}),
185            }],
186            complete: false,
187        };
188
189        let json = serde_json::to_value(&frame).unwrap();
190        assert_eq!(json["complete"], false);
191        assert_eq!(json["op"], "snapshot");
192    }
193
194    #[test]
195    fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
196        #[derive(Debug, Deserialize)]
197        struct TestSnapshotFrame {
198            #[allow(dead_code)]
199            mode: Mode,
200            #[allow(dead_code)]
201            #[serde(rename = "entity")]
202            export: String,
203            #[allow(dead_code)]
204            op: String,
205            #[allow(dead_code)]
206            data: Vec<SnapshotEntity>,
207            #[serde(default = "super::default_complete")]
208            complete: bool,
209        }
210
211        let json_without_complete = serde_json::json!({
212            "mode": "list",
213            "entity": "tokens/list",
214            "op": "snapshot",
215            "data": []
216        });
217
218        let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
219        assert!(frame.complete);
220    }
221
222    #[test]
223    fn test_snapshot_frame_batching_fields() {
224        let first_batch = SnapshotFrame {
225            mode: Mode::List,
226            export: "tokens/list".to_string(),
227            op: "snapshot",
228            data: vec![],
229            complete: false,
230        };
231
232        let final_batch = SnapshotFrame {
233            mode: Mode::List,
234            export: "tokens/list".to_string(),
235            op: "snapshot",
236            data: vec![],
237            complete: true,
238        };
239
240        assert!(!first_batch.complete);
241        assert!(final_batch.complete);
242    }
243}