Skip to main content

arete_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#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct WireFormat {
34    pub wide_int_paths: Vec<Vec<String>>,
35}
36
37/// Subscription acknowledgment frame sent when a client subscribes
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SubscribedFrame {
40    /// Operation type - always "subscribed"
41    pub op: &'static str,
42    /// The view that was subscribed to
43    pub view: String,
44    /// Streaming mode for this view
45    pub mode: Mode,
46    /// Sort configuration if this is a sorted view
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub sort: Option<SortConfig>,
49}
50
51impl SubscribedFrame {
52    pub fn new(view: String, mode: Mode, sort: Option<SortConfig>) -> Self {
53        Self {
54            op: "subscribed",
55            view,
56            mode,
57            sort,
58        }
59    }
60}
61
62/// Data frame sent over WebSocket
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Frame {
65    pub mode: Mode,
66    #[serde(rename = "entity")]
67    pub export: String,
68    pub op: &'static str,
69    pub key: String,
70    pub data: serde_json::Value,
71    #[serde(skip_serializing_if = "Vec::is_empty", default)]
72    pub append: Vec<String>,
73    /// Sequence cursor for ordering and resume capability
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub seq: Option<String>,
76}
77
78/// A single entity within a snapshot
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct SnapshotEntity {
81    pub key: String,
82    pub data: serde_json::Value,
83}
84
85/// Batch snapshot frame for initial data load
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct SnapshotFrame {
88    pub mode: Mode,
89    #[serde(rename = "entity")]
90    pub export: String,
91    pub op: &'static str,
92    pub data: Vec<SnapshotEntity>,
93    /// Indicates whether this is the final snapshot batch.
94    /// When `false`, more snapshot batches will follow.
95    /// When `true`, the snapshot is complete and live streaming begins.
96    #[serde(default = "default_complete")]
97    pub complete: bool,
98}
99
100fn default_complete() -> bool {
101    true
102}
103
104pub fn apply_wire_format(value: &mut serde_json::Value, wire_format: &WireFormat) {
105    for path in &wire_format.wide_int_paths {
106        stringify_value_at_path(value, path);
107    }
108}
109
110fn stringify_value_at_path(value: &mut serde_json::Value, path: &[String]) {
111    if path.is_empty() {
112        stringify_wide_int_value(value);
113        return;
114    }
115
116    match value {
117        serde_json::Value::Object(map) => {
118            if let Some(child) = map.get_mut(&path[0]) {
119                stringify_value_at_path(child, &path[1..]);
120            }
121        }
122        serde_json::Value::Array(values) => {
123            for child in values {
124                stringify_value_at_path(child, path);
125            }
126        }
127        _ => {}
128    }
129}
130
131fn stringify_wide_int_value(value: &mut serde_json::Value) {
132    match value {
133        serde_json::Value::Number(number) => {
134            if let Some(unsigned) = number.as_u64() {
135                *value = serde_json::Value::String(unsigned.to_string());
136            } else if let Some(signed) = number.as_i64() {
137                *value = serde_json::Value::String(signed.to_string());
138            }
139        }
140        serde_json::Value::Array(values) => {
141            for child in values {
142                stringify_wide_int_value(child);
143            }
144        }
145        _ => {}
146    }
147}
148
149impl Frame {
150    pub fn entity(&self) -> &str {
151        &self.export
152    }
153
154    pub fn key(&self) -> &str {
155        &self.key
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_frame_entity_key_accessors() {
165        let frame = Frame {
166            mode: Mode::List,
167            export: "SettlementGame/list".to_string(),
168            op: "upsert",
169            key: "123".to_string(),
170            data: serde_json::json!({}),
171            append: vec![],
172            seq: None,
173        };
174
175        assert_eq!(frame.entity(), "SettlementGame/list");
176        assert_eq!(frame.key(), "123");
177    }
178
179    #[test]
180    fn test_frame_serialization() {
181        let frame = Frame {
182            mode: Mode::List,
183            export: "SettlementGame/list".to_string(),
184            op: "upsert",
185            key: "123".to_string(),
186            data: serde_json::json!({"gameId": "123"}),
187            append: vec![],
188            seq: None,
189        };
190
191        let json = serde_json::to_value(&frame).unwrap();
192        assert_eq!(json["op"], "upsert");
193        assert_eq!(json["mode"], "list");
194        assert_eq!(json["entity"], "SettlementGame/list");
195        assert_eq!(json["key"], "123");
196    }
197
198    #[test]
199    fn test_frame_with_seq() {
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: Some("123456789:000000000042".to_string()),
208        };
209
210        let json = serde_json::to_value(&frame).unwrap();
211        assert_eq!(json["op"], "upsert");
212        assert_eq!(json["seq"], "123456789:000000000042");
213    }
214
215    #[test]
216    fn test_frame_seq_skipped_when_none() {
217        let frame = Frame {
218            mode: Mode::List,
219            export: "SettlementGame/list".to_string(),
220            op: "upsert",
221            key: "123".to_string(),
222            data: serde_json::json!({"gameId": "123"}),
223            append: vec![],
224            seq: None,
225        };
226
227        let json = serde_json::to_value(&frame).unwrap();
228        assert!(json.get("seq").is_none());
229    }
230
231    #[test]
232    fn test_snapshot_frame_complete_serialization() {
233        let frame = SnapshotFrame {
234            mode: Mode::List,
235            export: "tokens/list".to_string(),
236            op: "snapshot",
237            data: vec![SnapshotEntity {
238                key: "abc".to_string(),
239                data: serde_json::json!({"id": "abc"}),
240            }],
241            complete: false,
242        };
243
244        let json = serde_json::to_value(&frame).unwrap();
245        assert_eq!(json["complete"], false);
246        assert_eq!(json["op"], "snapshot");
247    }
248
249    #[test]
250    fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
251        #[derive(Debug, Deserialize)]
252        struct TestSnapshotFrame {
253            #[allow(dead_code)]
254            mode: Mode,
255            #[allow(dead_code)]
256            #[serde(rename = "entity")]
257            export: String,
258            #[allow(dead_code)]
259            op: String,
260            #[allow(dead_code)]
261            data: Vec<SnapshotEntity>,
262            #[serde(default = "super::default_complete")]
263            complete: bool,
264        }
265
266        let json_without_complete = serde_json::json!({
267            "mode": "list",
268            "entity": "tokens/list",
269            "op": "snapshot",
270            "data": []
271        });
272
273        let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
274        assert!(frame.complete);
275    }
276
277    #[test]
278    fn test_snapshot_frame_batching_fields() {
279        let first_batch = SnapshotFrame {
280            mode: Mode::List,
281            export: "tokens/list".to_string(),
282            op: "snapshot",
283            data: vec![],
284            complete: false,
285        };
286
287        let final_batch = SnapshotFrame {
288            mode: Mode::List,
289            export: "tokens/list".to_string(),
290            op: "snapshot",
291            data: vec![],
292            complete: true,
293        };
294
295        assert!(!first_batch.complete);
296        assert!(final_batch.complete);
297    }
298
299    #[test]
300    fn wire_format_stringifies_marked_wide_int_paths() {
301        let wire_format = WireFormat {
302            wide_int_paths: vec![
303                vec!["amount".to_string()],
304                vec!["nested".to_string(), "timestamp".to_string()],
305                vec!["values".to_string()],
306                vec!["positions".to_string(), "liquidity".to_string()],
307            ],
308        };
309
310        let mut value = serde_json::json!({
311            "amount": 42,
312            "nested": { "timestamp": -7 },
313            "values": [1, 2, 3],
314            "positions": [
315                { "liquidity": 9, "label": "a" },
316                { "liquidity": 11, "label": "b" }
317            ],
318            "small": 5,
319        });
320
321        apply_wire_format(&mut value, &wire_format);
322
323        assert_eq!(value["amount"], serde_json::Value::String("42".to_string()));
324        assert_eq!(
325            value["nested"]["timestamp"],
326            serde_json::Value::String("-7".to_string())
327        );
328        assert_eq!(
329            value["values"][0],
330            serde_json::Value::String("1".to_string())
331        );
332        assert_eq!(
333            value["positions"][1]["liquidity"],
334            serde_json::Value::String("11".to_string())
335        );
336        assert_eq!(value["small"], serde_json::json!(5));
337    }
338}