1use serde_json::{Map, Value};
7
8use crate::error::{DecodeError, Result};
9use crate::primitives::I64_SAFE_MAX;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum WakeReason {
14 DeltaTooLarge,
15 CatchupRequired,
16 ResetRequired,
17}
18
19impl WakeReason {
20 pub fn as_str(self) -> &'static str {
21 match self {
22 WakeReason::DeltaTooLarge => "delta-too-large",
23 WakeReason::CatchupRequired => "catchup-required",
24 WakeReason::ResetRequired => "reset-required",
25 }
26 }
27
28 pub fn from_reason(s: &str) -> Option<Self> {
29 match s {
30 "delta-too-large" => Some(WakeReason::DeltaTooLarge),
31 "catchup-required" => Some(WakeReason::CatchupRequired),
32 "reset-required" => Some(WakeReason::ResetRequired),
33 _ => None,
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub enum ControlMessage {
40 Hello {
42 protocol_version: i64,
43 session_id: String,
44 actor_id: String,
45 client_id: String,
46 cursor: i64,
47 latest_cursor: i64,
48 requires_sync: bool,
49 timestamp: i64,
50 },
51 Wake {
53 cursor: i64,
54 requires_pull: bool,
55 reason: WakeReason,
56 timestamp: i64,
57 },
58 Heartbeat { timestamp: i64 },
60 Ack { cursor: i64 },
62 Presence {
64 scope_key: String,
65 kind: Option<PresenceKind>,
67 actor_id: Option<String>,
68 client_id: Option<String>,
69 doc: Option<Value>,
71 error: Option<String>,
74 timestamp: Option<i64>,
75 },
76 Unknown(Value),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PresenceKind {
83 Join,
84 Update,
85 Leave,
86}
87
88impl PresenceKind {
89 fn from_str(s: &str) -> Option<Self> {
90 match s {
91 "join" => Some(Self::Join),
92 "update" => Some(Self::Update),
93 "leave" => Some(Self::Leave),
94 _ => None,
95 }
96 }
97
98 pub fn as_str(self) -> &'static str {
99 match self {
100 Self::Join => "join",
101 Self::Update => "update",
102 Self::Leave => "leave",
103 }
104 }
105}
106
107fn field<'a>(data: &'a Map<String, Value>, key: &str) -> Result<&'a Value> {
108 data.get(key)
109 .ok_or_else(|| DecodeError::invalid(format!("control message missing field {key:?}")))
110}
111
112fn as_i64(data: &Map<String, Value>, key: &str) -> Result<i64> {
113 let v = field(data, key)?.as_i64().ok_or_else(|| {
116 DecodeError::invalid(format!(
117 "control field {key:?} is not an integer within the i64 safe range"
118 ))
119 })?;
120 if !(-I64_SAFE_MAX..=I64_SAFE_MAX).contains(&v) {
121 return Err(DecodeError::invalid(format!(
122 "control field {key:?} = {v} outside the ±(2^53−1) safe-integer contract"
123 )));
124 }
125 Ok(v)
126}
127
128fn as_str(data: &Map<String, Value>, key: &str) -> Result<String> {
129 Ok(field(data, key)?
130 .as_str()
131 .ok_or_else(|| DecodeError::invalid(format!("control field {key:?} is not a string")))?
132 .to_owned())
133}
134
135fn as_bool(data: &Map<String, Value>, key: &str) -> Result<bool> {
136 field(data, key)?
137 .as_bool()
138 .ok_or_else(|| DecodeError::invalid(format!("control field {key:?} is not a boolean")))
139}
140
141pub fn parse_control(text: &str) -> Result<ControlMessage> {
143 let value: Value = serde_json::from_str(text)
144 .map_err(|e| DecodeError::invalid(format!("control message is not JSON: {e}")))?;
145 parse_control_value(&value)
146}
147
148pub fn parse_control_value(value: &Value) -> Result<ControlMessage> {
149 let root = value
150 .as_object()
151 .ok_or_else(|| DecodeError::invalid("control message is not a JSON object"))?;
152
153 if root.get("type").and_then(Value::as_str) == Some("ack") {
155 return Ok(ControlMessage::Ack {
156 cursor: as_i64(root, "cursor")?,
157 });
158 }
159
160 let Some(event) = root.get("event").and_then(Value::as_str) else {
161 return Err(DecodeError::invalid(
162 "control message has neither an \"event\" nor a known \"type\"",
163 ));
164 };
165 let data = |what: &str| -> Result<&Map<String, Value>> {
166 root.get("data")
167 .and_then(Value::as_object)
168 .ok_or_else(|| DecodeError::invalid(format!("{what} control message missing data")))
169 };
170 match event {
171 "hello" => {
172 let d = data("hello")?;
173 Ok(ControlMessage::Hello {
174 protocol_version: as_i64(d, "protocolVersion")?,
175 session_id: as_str(d, "sessionId")?,
176 actor_id: as_str(d, "actorId")?,
177 client_id: as_str(d, "clientId")?,
178 cursor: as_i64(d, "cursor")?,
179 latest_cursor: as_i64(d, "latestCursor")?,
180 requires_sync: as_bool(d, "requiresSync")?,
181 timestamp: as_i64(d, "timestamp")?,
182 })
183 }
184 "sync" => {
185 let d = data("sync")?;
186 let reason_raw = as_str(d, "reason")?;
187 let reason = WakeReason::from_reason(&reason_raw).ok_or_else(|| {
188 DecodeError::invalid(format!("unknown wake-up reason {reason_raw:?}"))
189 })?;
190 if !as_bool(d, "requiresPull")? {
193 return Err(DecodeError::invalid(
194 "sync.data.requiresPull must be the literal true",
195 ));
196 }
197 Ok(ControlMessage::Wake {
198 cursor: as_i64(d, "cursor")?,
199 requires_pull: true,
200 reason,
201 timestamp: as_i64(d, "timestamp")?,
202 })
203 }
204 "heartbeat" => {
205 let d = data("heartbeat")?;
206 Ok(ControlMessage::Heartbeat {
207 timestamp: as_i64(d, "timestamp")?,
208 })
209 }
210 "presence" => {
211 let d = data("presence")?;
212 let scope_key = as_str(d, "scopeKey")?;
213 if scope_key.is_empty() {
214 return Err(DecodeError::invalid(
215 "presence.data.scopeKey must be non-empty",
216 ));
217 }
218 let timestamp = if d.contains_key("timestamp") {
219 Some(as_i64(d, "timestamp")?)
220 } else {
221 None
222 };
223 if let Some(error) = d.get("error") {
224 let error = error
226 .as_str()
227 .ok_or_else(|| DecodeError::invalid("presence.data.error must be a string"))?
228 .to_owned();
229 return Ok(ControlMessage::Presence {
230 scope_key,
231 kind: None,
232 actor_id: None,
233 client_id: None,
234 doc: None,
235 error: Some(error),
236 timestamp,
237 });
238 }
239 match d.get("kind") {
240 Some(kind_val) => {
241 let kind_raw = kind_val.as_str().ok_or_else(|| {
243 DecodeError::invalid("presence.data.kind must be a string")
244 })?;
245 let kind = PresenceKind::from_str(kind_raw).ok_or_else(|| {
246 DecodeError::invalid(format!("unknown presence kind {kind_raw:?}"))
247 })?;
248 let actor_id = as_str(d, "actorId")?;
249 let client_id = as_str(d, "clientId")?;
250 let doc = d.get("doc").cloned();
251 match kind {
253 PresenceKind::Leave => {
254 if matches!(doc, Some(Value::Object(_))) {
255 return Err(DecodeError::invalid(
256 "presence leave must carry doc: null",
257 ));
258 }
259 }
260 _ => {
261 if !matches!(doc, Some(Value::Object(_))) {
262 return Err(DecodeError::invalid(
263 "presence.data.doc must be a JSON object",
264 ));
265 }
266 }
267 }
268 Ok(ControlMessage::Presence {
269 scope_key,
270 kind: Some(kind),
271 actor_id: Some(actor_id),
272 client_id: Some(client_id),
273 doc,
274 error: None,
275 timestamp,
276 })
277 }
278 None => {
279 let doc = d.get("doc").cloned();
281 if !matches!(doc, None | Some(Value::Null) | Some(Value::Object(_))) {
282 return Err(DecodeError::invalid(
283 "presence.data.doc must be a JSON object or null",
284 ));
285 }
286 Ok(ControlMessage::Presence {
287 scope_key,
288 kind: None,
289 actor_id: None,
290 client_id: None,
291 doc,
292 error: None,
293 timestamp,
294 })
295 }
296 }
297 }
298 _ => Ok(ControlMessage::Unknown(value.clone())),
299 }
300}
301
302pub fn encode_presence_publish(scope_key: &str, doc: Option<&Value>) -> String {
304 let mut d = Map::new();
305 d.insert("scopeKey".to_string(), Value::from(scope_key));
306 d.insert("doc".to_string(), doc.cloned().unwrap_or(Value::Null));
307 let mut root = Map::new();
308 root.insert("event".to_string(), Value::from("presence"));
309 root.insert("data".to_string(), Value::Object(d));
310 Value::Object(root).to_string()
311}
312
313pub fn render_control(msg: &ControlMessage) -> Value {
315 fn envelope(event: &str, data: Vec<(&str, Value)>) -> Value {
316 let mut d = Map::new();
317 for (k, v) in data {
318 d.insert(k.to_string(), v);
319 }
320 let mut root = Map::new();
321 root.insert("event".to_string(), Value::from(event));
322 root.insert("data".to_string(), Value::Object(d));
323 Value::Object(root)
324 }
325 match msg {
326 ControlMessage::Hello {
327 protocol_version,
328 session_id,
329 actor_id,
330 client_id,
331 cursor,
332 latest_cursor,
333 requires_sync,
334 timestamp,
335 } => envelope(
336 "hello",
337 vec![
338 ("protocolVersion", Value::from(*protocol_version)),
339 ("sessionId", Value::from(session_id.clone())),
340 ("actorId", Value::from(actor_id.clone())),
341 ("clientId", Value::from(client_id.clone())),
342 ("cursor", Value::from(*cursor)),
343 ("latestCursor", Value::from(*latest_cursor)),
344 ("requiresSync", Value::from(*requires_sync)),
345 ("timestamp", Value::from(*timestamp)),
346 ],
347 ),
348 ControlMessage::Wake {
349 cursor,
350 requires_pull,
351 reason,
352 timestamp,
353 } => envelope(
354 "sync",
355 vec![
356 ("cursor", Value::from(*cursor)),
357 ("requiresPull", Value::from(*requires_pull)),
358 ("reason", Value::from(reason.as_str())),
359 ("timestamp", Value::from(*timestamp)),
360 ],
361 ),
362 ControlMessage::Heartbeat { timestamp } => {
363 envelope("heartbeat", vec![("timestamp", Value::from(*timestamp))])
364 }
365 ControlMessage::Ack { cursor } => {
366 let mut root = Map::new();
367 root.insert("type".to_string(), Value::from("ack"));
368 root.insert("cursor".to_string(), Value::from(*cursor));
369 Value::Object(root)
370 }
371 ControlMessage::Presence {
372 scope_key,
373 kind,
374 actor_id,
375 client_id,
376 doc,
377 error,
378 timestamp,
379 } => {
380 let mut d = Map::new();
383 d.insert("scopeKey".to_string(), Value::from(scope_key.clone()));
384 if let Some(error) = error {
385 d.insert("error".to_string(), Value::from(error.clone()));
386 } else if let Some(kind) = kind {
387 d.insert("kind".to_string(), Value::from(kind.as_str()));
388 if let Some(actor_id) = actor_id {
389 d.insert("actorId".to_string(), Value::from(actor_id.clone()));
390 }
391 if let Some(client_id) = client_id {
392 d.insert("clientId".to_string(), Value::from(client_id.clone()));
393 }
394 d.insert("doc".to_string(), doc.clone().unwrap_or(Value::Null));
395 } else {
396 d.insert("doc".to_string(), doc.clone().unwrap_or(Value::Null));
398 }
399 if let Some(timestamp) = timestamp {
400 d.insert("timestamp".to_string(), Value::from(*timestamp));
401 }
402 let mut root = Map::new();
403 root.insert("event".to_string(), Value::from("presence"));
404 root.insert("data".to_string(), Value::Object(d));
405 Value::Object(root)
406 }
407 ControlMessage::Unknown(v) => v.clone(),
408 }
409}