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/// Data frame sent over WebSocket
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Frame {
18    pub mode: Mode,
19    #[serde(rename = "entity")]
20    pub export: String,
21    pub op: &'static str,
22    pub key: String,
23    pub data: serde_json::Value,
24    #[serde(skip_serializing_if = "Vec::is_empty", default)]
25    pub append: Vec<String>,
26}
27
28/// A single entity within a snapshot
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SnapshotEntity {
31    pub key: String,
32    pub data: serde_json::Value,
33}
34
35/// Batch snapshot frame for initial data load
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct SnapshotFrame {
38    pub mode: Mode,
39    #[serde(rename = "entity")]
40    pub export: String,
41    pub op: &'static str,
42    pub data: Vec<SnapshotEntity>,
43    /// Indicates whether this is the final snapshot batch.
44    /// When `false`, more snapshot batches will follow.
45    /// When `true`, the snapshot is complete and live streaming begins.
46    #[serde(default = "default_complete")]
47    pub complete: bool,
48}
49
50fn default_complete() -> bool {
51    true
52}
53
54impl Frame {
55    pub fn entity(&self) -> &str {
56        &self.export
57    }
58
59    pub fn key(&self) -> &str {
60        &self.key
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_frame_entity_key_accessors() {
70        let frame = Frame {
71            mode: Mode::List,
72            export: "SettlementGame/list".to_string(),
73            op: "upsert",
74            key: "123".to_string(),
75            data: serde_json::json!({}),
76            append: vec![],
77        };
78
79        assert_eq!(frame.entity(), "SettlementGame/list");
80        assert_eq!(frame.key(), "123");
81    }
82
83    #[test]
84    fn test_frame_serialization() {
85        let frame = Frame {
86            mode: Mode::List,
87            export: "SettlementGame/list".to_string(),
88            op: "upsert",
89            key: "123".to_string(),
90            data: serde_json::json!({"gameId": "123"}),
91            append: vec![],
92        };
93
94        let json = serde_json::to_value(&frame).unwrap();
95        assert_eq!(json["op"], "upsert");
96        assert_eq!(json["mode"], "list");
97        assert_eq!(json["entity"], "SettlementGame/list");
98        assert_eq!(json["key"], "123");
99    }
100
101    #[test]
102    fn test_snapshot_frame_complete_serialization() {
103        let frame = SnapshotFrame {
104            mode: Mode::List,
105            export: "tokens/list".to_string(),
106            op: "snapshot",
107            data: vec![SnapshotEntity {
108                key: "abc".to_string(),
109                data: serde_json::json!({"id": "abc"}),
110            }],
111            complete: false,
112        };
113
114        let json = serde_json::to_value(&frame).unwrap();
115        assert_eq!(json["complete"], false);
116        assert_eq!(json["op"], "snapshot");
117    }
118
119    #[test]
120    fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
121        #[derive(Debug, Deserialize)]
122        struct TestSnapshotFrame {
123            #[allow(dead_code)]
124            mode: Mode,
125            #[allow(dead_code)]
126            #[serde(rename = "entity")]
127            export: String,
128            #[allow(dead_code)]
129            op: String,
130            #[allow(dead_code)]
131            data: Vec<SnapshotEntity>,
132            #[serde(default = "super::default_complete")]
133            complete: bool,
134        }
135
136        let json_without_complete = serde_json::json!({
137            "mode": "list",
138            "entity": "tokens/list",
139            "op": "snapshot",
140            "data": []
141        });
142
143        let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
144        assert!(frame.complete);
145    }
146
147    #[test]
148    fn test_snapshot_frame_batching_fields() {
149        let first_batch = SnapshotFrame {
150            mode: Mode::List,
151            export: "tokens/list".to_string(),
152            op: "snapshot",
153            data: vec![],
154            complete: false,
155        };
156
157        let final_batch = SnapshotFrame {
158            mode: Mode::List,
159            export: "tokens/list".to_string(),
160            op: "snapshot",
161            data: vec![],
162            complete: true,
163        };
164
165        assert!(!first_batch.complete);
166        assert!(final_batch.complete);
167    }
168}