use bytes::Bytes;
use crate::error::ImError;
use crate::state::Seq;
pub(crate) const PONG_ACTION: &str = "__pong__";
pub(crate) struct WsFrame {
raw: Bytes,
root: serde_json::Value,
}
impl WsFrame {
pub(crate) fn parse(raw: &[u8]) -> Result<Self, ImError> {
let root: serde_json::Value = serde_json::from_slice(raw)
.map_err(|err| ImError::InvalidWsFrame(format!("invalid JSON: {err}")))?;
Ok(Self {
raw: Bytes::copy_from_slice(raw),
root,
})
}
pub(crate) fn action(&self) -> Result<&str, ImError> {
if let Some(action) = self.root.get("action").and_then(serde_json::Value::as_str) {
if action.starts_with("im:post_chain:") {
return Ok("post_chain");
}
return Ok(action);
}
if self
.root
.get("eventType")
.and_then(serde_json::Value::as_str)
.is_some_and(|event_type| event_type.starts_with("im:post_chain:"))
{
return Ok("post_chain");
}
if self.is_pong_frame() {
return Ok(PONG_ACTION);
}
Err(ImError::InvalidWsFrame("missing action".to_string()))
}
fn is_pong_frame(&self) -> bool {
let status_ok = self.root.get("status").and_then(serde_json::Value::as_str) == Some("OK");
if !status_ok {
return false;
}
self.data()
.map(|d| d.get("gaps").is_some() || d.get("hashMismatch").is_some())
.unwrap_or(false)
}
pub(crate) fn raw(&self) -> &[u8] {
&self.raw
}
pub(crate) fn root(&self) -> &serde_json::Value {
&self.root
}
pub(crate) fn data(&self) -> Option<&serde_json::Value> {
self.root.get("data")
}
pub(crate) fn data_required(&self) -> Result<&serde_json::Value, ImError> {
self.data()
.ok_or_else(|| ImError::InvalidWsFrame("missing data".to_string()))
}
pub(crate) fn cses_track_id(&self) -> Option<&str> {
let value = self
.root
.get("tracing")?
.get("csesTrackId")?
.as_str()?
.trim();
(!value.is_empty() && value.len() <= 64).then_some(value)
}
pub(crate) fn event_seq(&self) -> Option<Seq> {
self.data()
.and_then(|data| data.get("stream_seq"))
.and_then(serde_json::Value::as_u64)
.or_else(|| {
self.data()
.and_then(|data| data.get("streamSeq"))
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.data()
.and_then(|data| data.get("event_seq"))
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.data()
.and_then(|data| data.get("eventSeq"))
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.data()
.and_then(|data| data.get("props"))
.and_then(|props| props.get("channel_event_seq"))
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.root
.get("channel_event_seq")
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.root
.get("eventSeq")
.and_then(serde_json::Value::as_u64)
})
.or_else(|| {
self.root
.get("payload")
.and_then(|payload| payload.get("eventSeq"))
.and_then(serde_json::Value::as_u64)
})
.map(Seq)
}
pub(crate) fn delivery_seq(&self) -> Option<u64> {
self.root.get("seq").and_then(serde_json::Value::as_u64)
}
pub(crate) fn event_seq_required(&self) -> Result<Seq, ImError> {
self.event_seq()
.ok_or_else(|| ImError::InvalidWsFrame("missing event_seq".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_post_chain_action_and_camel_event_seq() {
let frame = WsFrame::parse(br#"{"action":"im:post_chain:upsert","data":{"eventSeq":17}}"#)
.expect("chain frame");
assert_eq!(frame.action().expect("chain action"), "post_chain");
assert_eq!(frame.event_seq(), Some(Seq(17)));
}
#[test]
fn keeps_root_delivery_sequence_out_of_domain_cursor() {
let frame =
WsFrame::parse(br#"{"action":"im:post_chain:upsert","seq":23,"event":{"seq":24}}"#)
.expect("chain frame");
assert_eq!(frame.delivery_seq(), Some(23));
assert_eq!(frame.event_seq(), None);
}
#[test]
fn reads_explicit_stream_sequence_independently_of_delivery_sequence() {
let frame = WsFrame::parse(br#"{"action":"post","seq":99,"data":{"stream_seq":23}}"#)
.expect("post frame");
assert_eq!(frame.delivery_seq(), Some(99));
assert_eq!(frame.event_seq(), Some(Seq(23)));
}
}