1use serde::Serialize;
8
9use crate::executor_contract::{MeshControlEnvelope, MeshObjectReadReason};
10
11pub const DATA_CONNECTED: &str = "dataConnected";
14pub const DATA_MESSAGE: &str = "dataMessage";
15pub const DATA_BINARY: &str = "dataBinary";
16pub const DATA_CLOSED: &str = "dataClosed";
17pub const DATA_ERROR: &str = "dataError";
18pub const DATA_INCOMING_CALL: &str = "dataIncomingCall";
19
20#[derive(Debug, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct DataConnectedPayload {
23 pub conn_id: u64,
24 pub peer: String,
25}
26
27#[derive(Debug, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct DataMessagePayload {
30 pub conn_id: u64,
31 pub data: String,
32}
33
34#[derive(Debug, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct DataBinaryPayload {
37 pub conn_id: u64,
38 pub data_base64: String,
39}
40
41#[derive(Debug, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct DataClosedPayload {
44 pub conn_id: u64,
45 pub reason: String,
46}
47
48#[derive(Debug, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct DataErrorPayload {
51 pub conn_id: u64,
52 pub error: String,
53}
54
55#[derive(Debug, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct DataIncomingCallPayload {
58 pub conn_id: u64,
59 pub peer: String,
60}
61
62pub const MEDIA_TRACK_READY: &str = "mediaTrackReady";
65pub const MEDIA_CONNECTED: &str = "mediaConnected";
66pub const MEDIA_REMOTE_TRACK: &str = "mediaRemoteTrack";
67pub const MEDIA_CLOSED: &str = "mediaClosed";
68pub const MEDIA_ERROR: &str = "mediaError";
69pub const MEDIA_INCOMING_CALL: &str = "mediaIncomingCall";
70pub const MEDIA_SIGNALING_PROGRESS: &str = "mediaSignalingProgress";
71pub const MEDIA_TRACK_STOPPED: &str = "mediaTrackStopped";
72
73#[derive(Debug, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct MediaTrackReadyPayload {
76 pub track_id: u64,
77 pub kind: String,
78}
79
80#[derive(Debug, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct MediaConnectedPayload {
83 pub session_id: u64,
84 pub peer: String,
85}
86
87#[derive(Debug, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct MediaRemoteTrackPayload {
90 pub session_id: u64,
91 pub track_id: u64,
92 pub kind: String,
93}
94
95#[derive(Debug, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct MediaClosedPayload {
98 pub session_id: u64,
99 pub reason: String,
100}
101
102#[derive(Debug, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct MediaErrorPayload {
105 pub session_id: u64,
106 pub error: String,
107}
108
109#[derive(Debug, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct MediaIncomingCallPayload {
112 pub peer: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub peer_address: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub group_id: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub group_size: Option<u32>,
119}
120
121#[derive(Debug, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct MediaSignalingProgressPayload {
124 pub session_id: u64,
125 pub stage: String,
126}
127
128#[derive(Debug, Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct MediaTrackStoppedPayload {
135 pub track_id: u64,
136 pub kind: String,
137 pub session_id: u64,
138}
139
140pub const STATEMENT: &str = "statement";
143
144#[derive(Debug, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct StatementPayload {
151 pub author: String,
152 pub channel: String,
153 pub data: String,
154 pub timestamp_ms: u64,
155}
156
157pub const MESH_TOPIC: &str = "meshTopic";
160pub const MESH_QUERY: &str = "meshQuery";
161pub const MESH_REPLY: &str = "meshReply";
162pub const MESH_PRESENCE: &str = "meshPresence";
163pub const MESH_PRIVATE_CONTROL: &str = "meshPrivateControl";
164pub const MESH_PRIVATE_RECEIPT: &str = "meshPrivateReceipt";
165
166#[derive(Debug, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct MeshTopicPayload {
169 pub topic: String,
170 pub data_base64: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub author: Option<String>,
173}
174
175#[derive(Debug, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub struct MeshQueryPayload {
178 pub request_id: String,
179 pub path: String,
180 pub data_base64: String,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub author: Option<String>,
183}
184
185#[derive(Debug, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct MeshReplyPayload {
188 pub request_id: String,
189 pub data_base64: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub reason: Option<MeshObjectReadReason>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub expires_at_ms: Option<u64>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub author: Option<String>,
196}
197
198#[derive(Debug, Serialize)]
199#[serde(rename_all = "camelCase")]
200pub struct MeshPresencePayload {
201 pub peer_id: String,
202 pub state: String,
203}
204
205#[derive(Debug, Serialize)]
206#[serde(rename_all = "camelCase")]
207pub struct MeshPrivateControlPayload {
208 pub capability: String,
209 pub envelope: MeshControlEnvelope,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub author: Option<String>,
212}
213
214pub const CRDT_REMOTE_UPDATE: &str = "crdtRemoteUpdate";
217pub const CRDT_AWARENESS: &str = "crdtAwareness";
218pub const CRDT_PEER_CHANGE: &str = "crdtPeerChange";
219
220#[derive(Debug, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct CrdtRemoteUpdatePayload {
223 pub room_id: String,
224 pub update_base64: String,
225}
226
227#[derive(Debug, Serialize)]
228#[serde(rename_all = "camelCase")]
229pub struct MeshPrivateReceiptPayload {
230 pub capability: String,
231 pub data_base64: String,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub author: Option<String>,
234}
235
236#[derive(Debug, Serialize)]
237#[serde(rename_all = "camelCase")]
238pub struct CrdtAwarenessPayload {
239 pub room_id: String,
240 pub client_id: u64,
241 pub state: String,
244}
245
246#[derive(Debug, Serialize)]
247#[serde(rename_all = "camelCase")]
248pub struct CrdtPeerChangePayload {
249 pub room_id: String,
250 pub peers: Vec<String>,
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn json(v: &impl serde::Serialize) -> String {
258 serde_json::to_string(v).unwrap()
259 }
260
261 #[test]
262 fn data_connected_json_shape() {
263 let p = DataConnectedPayload {
264 conn_id: 1,
265 peer: "alice".into(),
266 };
267 assert_eq!(json(&p), r#"{"connId":1,"peer":"alice"}"#);
268 }
269
270 #[test]
271 fn data_message_json_shape() {
272 let p = DataMessagePayload {
273 conn_id: 2,
274 data: "hello".into(),
275 };
276 assert_eq!(json(&p), r#"{"connId":2,"data":"hello"}"#);
277 }
278
279 #[test]
280 fn data_binary_json_shape() {
281 let p = DataBinaryPayload {
282 conn_id: 3,
283 data_base64: "AQID".into(),
284 };
285 assert_eq!(json(&p), r#"{"connId":3,"dataBase64":"AQID"}"#);
286 }
287
288 #[test]
289 fn data_closed_json_shape() {
290 let p = DataClosedPayload {
291 conn_id: 4,
292 reason: "done".into(),
293 };
294 assert_eq!(json(&p), r#"{"connId":4,"reason":"done"}"#);
295 }
296
297 #[test]
298 fn data_error_json_shape() {
299 let p = DataErrorPayload {
300 conn_id: 5,
301 error: "fail".into(),
302 };
303 assert_eq!(json(&p), r#"{"connId":5,"error":"fail"}"#);
304 }
305
306 #[test]
307 fn data_incoming_call_json_shape() {
308 let p = DataIncomingCallPayload {
309 conn_id: 6,
310 peer: "bob".into(),
311 };
312 assert_eq!(json(&p), r#"{"connId":6,"peer":"bob"}"#);
313 }
314
315 #[test]
316 fn media_track_ready_json_shape() {
317 let p = MediaTrackReadyPayload {
318 track_id: 1,
319 kind: "video".into(),
320 };
321 assert_eq!(json(&p), r#"{"trackId":1,"kind":"video"}"#);
322 }
323
324 #[test]
325 fn media_connected_json_shape() {
326 let p = MediaConnectedPayload {
327 session_id: 2,
328 peer: "carol".into(),
329 };
330 assert_eq!(json(&p), r#"{"sessionId":2,"peer":"carol"}"#);
331 }
332
333 #[test]
334 fn media_remote_track_json_shape() {
335 let p = MediaRemoteTrackPayload {
336 session_id: 3,
337 track_id: 10,
338 kind: "audio".into(),
339 };
340 assert_eq!(json(&p), r#"{"sessionId":3,"trackId":10,"kind":"audio"}"#);
341 }
342
343 #[test]
344 fn media_closed_json_shape() {
345 let p = MediaClosedPayload {
346 session_id: 4,
347 reason: "hangup".into(),
348 };
349 assert_eq!(json(&p), r#"{"sessionId":4,"reason":"hangup"}"#);
350 }
351
352 #[test]
353 fn media_error_json_shape() {
354 let p = MediaErrorPayload {
355 session_id: 5,
356 error: "timeout".into(),
357 };
358 assert_eq!(json(&p), r#"{"sessionId":5,"error":"timeout"}"#);
359 }
360
361 #[test]
362 fn media_incoming_call_json_shape() {
363 let p = MediaIncomingCallPayload {
364 peer: "dave".into(),
365 peer_address: None,
366 group_id: None,
367 group_size: None,
368 };
369 assert_eq!(json(&p), r#"{"peer":"dave"}"#);
370 }
371
372 #[test]
373 fn media_incoming_call_group_json_shape() {
374 let p = MediaIncomingCallPayload {
375 peer: "alice".into(),
376 peer_address: Some("alice".into()),
377 group_id: Some("grp1".into()),
378 group_size: Some(3),
379 };
380 assert_eq!(
381 json(&p),
382 r#"{"peer":"alice","peerAddress":"alice","groupId":"grp1","groupSize":3}"#
383 );
384 }
385
386 #[test]
387 fn media_incoming_call_1_to_1_omits_group_fields() {
388 let p = MediaIncomingCallPayload {
389 peer: "bob".into(),
390 peer_address: None,
391 group_id: None,
392 group_size: None,
393 };
394 assert_eq!(json(&p), r#"{"peer":"bob"}"#);
395 }
396
397 #[test]
398 fn media_signaling_progress_json_shape() {
399 let p = MediaSignalingProgressPayload {
400 session_id: 7,
401 stage: "ice".into(),
402 };
403 assert_eq!(json(&p), r#"{"sessionId":7,"stage":"ice"}"#);
404 }
405
406 #[test]
407 fn media_track_stopped_json_shape() {
408 let p = MediaTrackStoppedPayload {
409 track_id: 42,
410 kind: "screen".into(),
411 session_id: 0,
412 };
413 assert_eq!(json(&p), r#"{"trackId":42,"kind":"screen","sessionId":0}"#);
414 }
415
416 #[test]
417 fn statement_json_shape() {
418 let p = StatementPayload {
419 author: "alice".into(),
420 channel: "chat".into(),
421 data: "hello".into(),
422 timestamp_ms: 1710000000000,
423 };
424 assert_eq!(
425 json(&p),
426 r#"{"author":"alice","channel":"chat","data":"hello","timestampMs":1710000000000}"#
427 );
428 }
429
430 #[test]
431 fn mesh_topic_json_shape() {
432 let payload = MeshTopicPayload {
433 topic: "room/1".into(),
434 data_base64: "AQID".into(),
435 author: Some("alice".into()),
436 };
437 assert_eq!(
438 json(&payload),
439 r#"{"topic":"room/1","dataBase64":"AQID","author":"alice"}"#
440 );
441 }
442
443 #[test]
444 fn mesh_query_json_shape() {
445 let payload = MeshQueryPayload {
446 request_id: "req-1".into(),
447 path: "mesh/object/1".into(),
448 data_base64: "AQID".into(),
449 author: None,
450 };
451 assert_eq!(
452 json(&payload),
453 r#"{"requestId":"req-1","path":"mesh/object/1","dataBase64":"AQID"}"#
454 );
455 }
456
457 #[test]
458 fn mesh_reply_json_shape() {
459 let payload = MeshReplyPayload {
460 request_id: "req-1".into(),
461 data_base64: Some("AQID".into()),
462 reason: None,
463 expires_at_ms: None,
464 author: Some("bob".into()),
465 };
466 assert_eq!(
467 json(&payload),
468 r#"{"requestId":"req-1","dataBase64":"AQID","author":"bob"}"#
469 );
470 }
471
472 #[test]
473 fn mesh_reply_negative_json_shape() {
474 let payload = MeshReplyPayload {
475 request_id: "req-2".into(),
476 data_base64: None,
477 reason: Some(MeshObjectReadReason::Expired),
478 expires_at_ms: Some(1710000000000),
479 author: None,
480 };
481 assert_eq!(
482 json(&payload),
483 r#"{"requestId":"req-2","dataBase64":null,"reason":"expired","expiresAtMs":1710000000000}"#
484 );
485 }
486
487 #[test]
488 fn mesh_presence_json_shape() {
489 let payload = MeshPresencePayload {
490 peer_id: "peer-1".into(),
491 state: "up".into(),
492 };
493 assert_eq!(json(&payload), r#"{"peerId":"peer-1","state":"up"}"#);
494 }
495
496 #[test]
497 fn mesh_private_control_json_shape() {
498 let payload = MeshPrivateControlPayload {
499 capability: "mesh-private-capability-1".into(),
500 envelope: MeshControlEnvelope {
501 mode: crate::executor_contract::MeshControlMode::Encrypted,
502 data_base64: "AQID".into(),
503 },
504 author: Some("alice".into()),
505 };
506 assert_eq!(
507 json(&payload),
508 r#"{"capability":"mesh-private-capability-1","envelope":{"mode":"encrypted","dataBase64":"AQID"},"author":"alice"}"#
509 );
510 }
511
512 #[test]
513 fn mesh_private_receipt_json_shape() {
514 let payload = MeshPrivateReceiptPayload {
515 capability: "mesh-private-capability-1".into(),
516 data_base64: "AQID".into(),
517 author: None,
518 };
519 assert_eq!(
520 json(&payload),
521 r#"{"capability":"mesh-private-capability-1","dataBase64":"AQID"}"#
522 );
523 }
524
525 #[test]
526 fn crdt_remote_update_json_shape() {
527 let p = CrdtRemoteUpdatePayload {
528 room_id: "doc-abc".into(),
529 update_base64: "AQID".into(),
530 };
531 assert_eq!(json(&p), r#"{"roomId":"doc-abc","updateBase64":"AQID"}"#);
532 }
533
534 #[test]
535 fn crdt_awareness_json_shape() {
536 let p = CrdtAwarenessPayload {
537 room_id: "doc-abc".into(),
538 client_id: 42,
539 state: r#"{"cursor":{"index":5}}"#.into(),
540 };
541 assert_eq!(
542 json(&p),
543 r#"{"roomId":"doc-abc","clientId":42,"state":"{\"cursor\":{\"index\":5}}"}"#
544 );
545 }
546
547 #[test]
548 fn crdt_peer_change_json_shape() {
549 let p = CrdtPeerChangePayload {
550 room_id: "doc-abc".into(),
551 peers: vec!["alice".into(), "bob".into()],
552 };
553 assert_eq!(json(&p), r#"{"roomId":"doc-abc","peers":["alice","bob"]}"#);
554 }
555
556 #[test]
557 fn crdt_peer_change_empty_peers_json_shape() {
558 let p = CrdtPeerChangePayload {
559 room_id: "doc-abc".into(),
560 peers: vec![],
561 };
562 assert_eq!(json(&p), r#"{"roomId":"doc-abc","peers":[]}"#);
563 }
564}