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    /// Sequence cursor for ordering and resume capability
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub seq: Option<String>,
71}
72
73/// A single entity within a snapshot
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SnapshotEntity {
76    pub key: String,
77    pub data: serde_json::Value,
78}
79
80/// Batch snapshot frame for initial data load
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct SnapshotFrame {
83    pub mode: Mode,
84    #[serde(rename = "entity")]
85    pub export: String,
86    pub op: &'static str,
87    pub data: Vec<SnapshotEntity>,
88    /// Indicates whether this is the final snapshot batch.
89    /// When `false`, more snapshot batches will follow.
90    /// When `true`, the snapshot is complete and live streaming begins.
91    #[serde(default = "default_complete")]
92    pub complete: bool,
93}
94
95fn default_complete() -> bool {
96    true
97}
98
99/// Transform large u64 values to strings for JavaScript compatibility.
100/// JavaScript's Number.MAX_SAFE_INTEGER is 2^53 - 1 (9007199254740991).
101/// Values larger than this will lose precision in JavaScript.
102pub fn transform_large_u64_to_strings(value: &mut serde_json::Value) {
103    const MAX_SAFE_INTEGER: u64 = 9007199254740991; // 2^53 - 1
104
105    match value {
106        serde_json::Value::Object(map) => {
107            for (_, v) in map.iter_mut() {
108                transform_large_u64_to_strings(v);
109            }
110        }
111        serde_json::Value::Array(arr) => {
112            for v in arr.iter_mut() {
113                transform_large_u64_to_strings(v);
114            }
115        }
116        serde_json::Value::Number(n) => {
117            if let Some(n_u64) = n.as_u64() {
118                if n_u64 > MAX_SAFE_INTEGER {
119                    *value = serde_json::Value::String(n_u64.to_string());
120                }
121            } else if let Some(n_i64) = n.as_i64() {
122                const MIN_SAFE_INTEGER: i64 = -(MAX_SAFE_INTEGER as i64);
123                if n_i64 < MIN_SAFE_INTEGER {
124                    *value = serde_json::Value::String(n_i64.to_string());
125                }
126            }
127        }
128        _ => {}
129    }
130}
131
132impl Frame {
133    pub fn entity(&self) -> &str {
134        &self.export
135    }
136
137    pub fn key(&self) -> &str {
138        &self.key
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_frame_entity_key_accessors() {
148        let frame = Frame {
149            mode: Mode::List,
150            export: "SettlementGame/list".to_string(),
151            op: "upsert",
152            key: "123".to_string(),
153            data: serde_json::json!({}),
154            append: vec![],
155            seq: None,
156        };
157
158        assert_eq!(frame.entity(), "SettlementGame/list");
159        assert_eq!(frame.key(), "123");
160    }
161
162    #[test]
163    fn test_frame_serialization() {
164        let frame = Frame {
165            mode: Mode::List,
166            export: "SettlementGame/list".to_string(),
167            op: "upsert",
168            key: "123".to_string(),
169            data: serde_json::json!({"gameId": "123"}),
170            append: vec![],
171            seq: None,
172        };
173
174        let json = serde_json::to_value(&frame).unwrap();
175        assert_eq!(json["op"], "upsert");
176        assert_eq!(json["mode"], "list");
177        assert_eq!(json["entity"], "SettlementGame/list");
178        assert_eq!(json["key"], "123");
179    }
180
181    #[test]
182    fn test_frame_with_seq() {
183        let frame = Frame {
184            mode: Mode::List,
185            export: "SettlementGame/list".to_string(),
186            op: "upsert",
187            key: "123".to_string(),
188            data: serde_json::json!({"gameId": "123"}),
189            append: vec![],
190            seq: Some("123456789:000000000042".to_string()),
191        };
192
193        let json = serde_json::to_value(&frame).unwrap();
194        assert_eq!(json["op"], "upsert");
195        assert_eq!(json["seq"], "123456789:000000000042");
196    }
197
198    #[test]
199    fn test_frame_seq_skipped_when_none() {
200        let frame = Frame {
201            mode: Mode::List,
202            export: "SettlementGame/list".to_string(),
203            op: "upsert",
204            key: "123".to_string(),
205            data: serde_json::json!({"gameId": "123"}),
206            append: vec![],
207            seq: None,
208        };
209
210        let json = serde_json::to_value(&frame).unwrap();
211        assert!(json.get("seq").is_none());
212    }
213
214    #[test]
215    fn test_snapshot_frame_complete_serialization() {
216        let frame = SnapshotFrame {
217            mode: Mode::List,
218            export: "tokens/list".to_string(),
219            op: "snapshot",
220            data: vec![SnapshotEntity {
221                key: "abc".to_string(),
222                data: serde_json::json!({"id": "abc"}),
223            }],
224            complete: false,
225        };
226
227        let json = serde_json::to_value(&frame).unwrap();
228        assert_eq!(json["complete"], false);
229        assert_eq!(json["op"], "snapshot");
230    }
231
232    #[test]
233    fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
234        #[derive(Debug, Deserialize)]
235        struct TestSnapshotFrame {
236            #[allow(dead_code)]
237            mode: Mode,
238            #[allow(dead_code)]
239            #[serde(rename = "entity")]
240            export: String,
241            #[allow(dead_code)]
242            op: String,
243            #[allow(dead_code)]
244            data: Vec<SnapshotEntity>,
245            #[serde(default = "super::default_complete")]
246            complete: bool,
247        }
248
249        let json_without_complete = serde_json::json!({
250            "mode": "list",
251            "entity": "tokens/list",
252            "op": "snapshot",
253            "data": []
254        });
255
256        let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
257        assert!(frame.complete);
258    }
259
260    #[test]
261    fn test_snapshot_frame_batching_fields() {
262        let first_batch = SnapshotFrame {
263            mode: Mode::List,
264            export: "tokens/list".to_string(),
265            op: "snapshot",
266            data: vec![],
267            complete: false,
268        };
269
270        let final_batch = SnapshotFrame {
271            mode: Mode::List,
272            export: "tokens/list".to_string(),
273            op: "snapshot",
274            data: vec![],
275            complete: true,
276        };
277
278        assert!(!first_batch.complete);
279        assert!(final_batch.complete);
280    }
281}