Skip to main content

rz_protocol/
lib.rs

1//! Wire protocol: JSON envelopes with `@@RZ:` sentinel.
2//!
3//! Every protocol message is a single line:
4//! ```text
5//! @@RZ:{"id":"...","from":"...","kind":{"kind":"chat","body":{"text":"..."}}}
6//! ```
7//! The `@@RZ:` prefix lets receivers distinguish protocol messages from
8//! normal shell output or human typing.
9
10use serde::{Deserialize, Serialize};
11use std::sync::atomic::{AtomicU32, Ordering};
12
13pub const SENTINEL: &str = "@@RZ:";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Envelope {
17    pub id: String,
18    pub from: String,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub r#ref: Option<String>,
21    pub kind: MessageKind,
22    pub ts: u64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(tag = "kind", content = "body", rename_all = "snake_case")]
27pub enum MessageKind {
28    Chat { text: String },
29    Hello { name: String, pane_id: String },
30    Ping,
31    Pong,
32    Error { message: String },
33    Timer { label: String },
34}
35
36static COUNTER: AtomicU32 = AtomicU32::new(0);
37
38impl Envelope {
39    pub fn new(from: impl Into<String>, kind: MessageKind) -> Self {
40        let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
41        let ts = std::time::SystemTime::now()
42            .duration_since(std::time::UNIX_EPOCH)
43            .unwrap_or_default()
44            .as_millis() as u64;
45        Self {
46            id: format!("{:04x}{:04x}", (ts & 0xFFFF) as u16, seq),
47            r#ref: None,
48            from: from.into(),
49            kind,
50            ts,
51        }
52    }
53
54    /// Set the `ref` field (builder pattern).
55    pub fn with_ref(mut self, r: impl Into<String>) -> Self {
56        self.r#ref = Some(r.into());
57        self
58    }
59
60    /// Conditionally set `ref` if `Some`.
61    pub fn maybe_with_ref(mut self, r: Option<String>) -> Self {
62        self.r#ref = r;
63        self
64    }
65
66    /// Encode to wire format: `@@RZ:<json>`
67    pub fn encode(&self) -> eyre::Result<String> {
68        let json = serde_json::to_string(self)?;
69        Ok(format!("{SENTINEL}{json}"))
70    }
71
72    /// Decode from wire format (with or without sentinel prefix).
73    pub fn decode(line: &str) -> eyre::Result<Self> {
74        let payload = line.strip_prefix(SENTINEL).unwrap_or(line);
75        Ok(serde_json::from_str(payload.trim())?)
76    }
77}