use serde_json::json;
use super::{FramePayload, Refusal, SseEvent};
use crate::domain::{EventId, EventRing, Replay, StreamEvent};
const MINIMAL_GAP_NOTICE: &str = r#"{"type":"resume-gap"}"#;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resume {
Replay {
frames: Vec<SseEvent>,
last: EventId,
},
Live {
last: EventId,
},
Gap {
notice: SseEvent,
evicted_through: EventId,
resume_from: EventId,
last: EventId,
},
Refused(Refusal),
}
impl Resume {
#[must_use]
pub fn pending(&self) -> &[SseEvent] {
match self {
Self::Replay { frames, .. } => frames,
Self::Gap { notice, .. } => core::slice::from_ref(notice),
Self::Live { .. } | Self::Refused(_) => &[],
}
}
#[must_use]
pub const fn last(&self) -> Option<EventId> {
match self {
Self::Replay { last, .. } | Self::Live { last } | Self::Gap { last, .. } => Some(*last),
Self::Refused(_) => None,
}
}
}
#[must_use]
pub fn plan<T: FramePayload>(ring: &EventRing<T>, header: Option<&str>) -> Resume {
let Some(header) = header else {
return decide(&ring.replay_from(None));
};
parse_cursor(header).map_or(Resume::Refused(Refusal::MalformedCursor), |cursor| {
decide(&ring.replay_from(Some(cursor)))
})
}
#[must_use]
pub fn decide<T: FramePayload>(replay: &Replay<'_, T>) -> Resume {
match replay {
Replay::UpToDate { last } => Resume::Live { last: *last },
Replay::Buffered { events, last } => buffered(events, *last),
Replay::Gap {
evicted_through,
last,
..
} => gap(*evicted_through, *last),
Replay::Unknown { requested, last } => Resume::Refused(Refusal::UnknownCursor {
requested: *requested,
last: *last,
}),
}
}
fn parse_cursor(header: &str) -> Option<EventId> {
if header.contains(['\n', '\r']) {
return None;
}
let block = format!("id: {header}\ndata: {{}}\n");
SseEvent::dispatch(&block).ok()??.id()
}
fn buffered<T: FramePayload>(events: &[StreamEvent<T>], last: EventId) -> Resume {
let mut frames = Vec::with_capacity(events.len());
for event in events {
match event.payload().to_frame(event.id()) {
Ok(frame) => frames.push(frame),
Err(_) => {
return Resume::Refused(Refusal::Handler(
"an event payload has no JSON form".to_owned(),
));
}
}
}
Resume::Replay { frames, last }
}
fn gap(evicted_through: EventId, last: EventId) -> Resume {
let resume_from = EventId::new(evicted_through.get().saturating_add(1));
let notice = gap_notice(evicted_through, resume_from, last);
Resume::Gap {
notice,
evicted_through,
resume_from,
last,
}
}
fn gap_notice(evicted_through: EventId, resume_from: EventId, last: EventId) -> SseEvent {
let value = json!({
"type": "resume-gap",
"evictedThrough": evicted_through.get(),
"resumeFrom": resume_from.get(),
"lastEventId": last.get(),
});
let data = serde_json::to_string(&value).unwrap_or_else(|_| MINIMAL_GAP_NOTICE.to_owned());
SseEvent::without_id(data)
}
#[cfg(test)]
mod tests {
use core::num::NonZeroUsize;
use axum::http::StatusCode;
use serde_json::{Value, json};
use super::{Resume, decide, plan};
use crate::adapters::primary::transport::http::{Refusal, SseEvent};
use crate::domain::{EventId, EventRing};
type TestError = Box<dyn std::error::Error>;
fn ring_of(values: &[Value]) -> Result<EventRing<Value>, TestError> {
let capacity =
NonZeroUsize::new(values.len().max(1)).ok_or("a ring needs a non-zero capacity")?;
let mut ring = EventRing::new(capacity);
for value in values {
ring.push(value.clone());
}
Ok(ring)
}
fn three_events() -> Vec<Value> {
vec![json!({"seq": 1}), json!({"seq": 2}), json!({"seq": 3})]
}
fn frame_ids(decision: &Resume) -> Vec<Option<EventId>> {
decision.pending().iter().map(SseEvent::id).collect()
}
fn at<'a>(body: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut node = body;
for key in path {
node = node.get(key)?;
}
Some(node)
}
fn slug_of(refusal: &Refusal) -> Option<String> {
let body = refusal.body();
at(&body, &["error", "data", "error"])
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}
#[test]
fn a_fresh_client_with_no_header_gets_the_whole_window() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
let decision = plan(&ring, None);
let Resume::Replay { frames, last } = &decision else {
return Err(format!("expected a replay, got {decision:?}").into());
};
assert_eq!(frames.len(), 3);
assert_eq!(*last, EventId::new(3));
Ok(())
}
#[test]
fn a_client_exactly_at_the_end_is_held_open_and_writes_nothing() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
let decision = plan(&ring, Some("3"));
assert_eq!(
decision,
Resume::Live {
last: EventId::new(3)
}
);
assert!(decision.pending().is_empty());
assert_eq!(decision.last(), Some(EventId::new(3)));
Ok(())
}
#[test]
fn a_client_quoting_a_dropped_event_is_told_it_missed_them() -> Result<(), TestError> {
let mut ring = EventRing::new(NonZeroUsize::new(2).ok_or("capacity")?);
for seq in 1..=5 {
ring.push(json!({ "seq": seq }));
}
let Resume::Gap {
evicted_through,
resume_from,
last,
..
} = plan(&ring, Some("1"))
else {
return Err("expected a gap".into());
};
assert_eq!(evicted_through, EventId::new(3));
assert_eq!(resume_from, EventId::new(4));
assert_eq!(last, EventId::new(5));
let retry = plan(&ring, Some(&resume_from.get().to_string()));
let Resume::Replay { .. } = &retry else {
return Err(format!("expected a complete replay, got {retry:?}").into());
};
assert_eq!(frame_ids(&retry), vec![Some(EventId::new(5))]);
Ok(())
}
#[test]
fn the_gap_notice_is_dispatched_and_carries_no_id() -> Result<(), TestError> {
let mut ring = EventRing::new(NonZeroUsize::new(2).ok_or("capacity")?);
for seq in 1..=5 {
ring.push(json!({ "seq": seq }));
}
let decision = plan(&ring, Some("1"));
let pending = decision.pending();
let [notice] = pending else {
return Err(format!("expected exactly one frame, got {pending:?}").into());
};
assert_eq!(notice.id(), None, "the gap notice must not move the cursor");
assert_eq!(frame_ids(&decision), vec![None]);
let encoded = notice.encode();
assert!(
!encoded.starts_with(':'),
"a comment dispatches nothing, so the client would never see the gap"
);
assert!(
!encoded.starts_with("id:"),
"an id here would claim events the client never received"
);
assert!(encoded.starts_with("data:"));
let body: Value = serde_json::from_str(notice.data())?;
assert_eq!(at(&body, &["type"]), Some(&json!("resume-gap")));
assert_eq!(at(&body, &["evictedThrough"]), Some(&json!(3)));
assert_eq!(at(&body, &["resumeFrom"]), Some(&json!(4)));
assert_eq!(at(&body, &["lastEventId"]), Some(&json!(5)));
Ok(())
}
#[test]
fn a_client_quoting_an_id_past_the_end_is_refused() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
let decision = plan(&ring, Some("99"));
assert_eq!(
decision,
Resume::Refused(Refusal::UnknownCursor {
requested: EventId::new(99),
last: EventId::new(3),
})
);
let Resume::Refused(refusal) = &decision else {
return Err("expected a refusal".into());
};
assert_eq!(refusal.status(), StatusCode::CONFLICT);
assert_eq!(slug_of(refusal).as_deref(), Some("unknown_cursor"));
assert!(decision.pending().is_empty());
assert_eq!(decision.last(), None);
Ok(())
}
#[test]
fn a_corrupt_cursor_is_refused_rather_than_read_as_a_fresh_start() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
for corrupt in ["abc", "", "1x", " ", "-1", "99999999999999999999999"] {
let decision = plan(&ring, Some(corrupt));
assert_eq!(
decision,
Resume::Refused(Refusal::MalformedCursor),
"expected {corrupt:?} to be refused"
);
let Resume::Refused(refusal) = &decision else {
return Err("expected a refusal".into());
};
assert_eq!(refusal.status(), StatusCode::BAD_REQUEST);
assert_eq!(slug_of(refusal).as_deref(), Some("malformed_cursor"));
assert!(decision.pending().is_empty());
assert_eq!(decision.last(), None);
}
Ok(())
}
#[test]
fn a_cursor_with_a_line_terminator_cannot_inject_a_second_id() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
for injected in ["1\nid: 99", "1\r\nid: 99", "abc\ndata: forged"] {
let decision = plan(&ring, Some(injected));
assert_eq!(
decision,
Resume::Refused(Refusal::MalformedCursor),
"expected {injected:?} to be refused"
);
}
Ok(())
}
#[test]
fn every_replayed_frame_keeps_its_own_id() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
let decision = plan(&ring, Some("1"));
assert_eq!(
frame_ids(&decision),
vec![Some(EventId::new(2)), Some(EventId::new(3))]
);
for frame in decision.pending() {
assert!(frame.encode().starts_with("id: "));
}
Ok(())
}
#[test]
fn a_payload_containing_a_newline_survives_the_round_trip() -> Result<(), TestError> {
let original = json!({ "text": "one\ntwo\nthree" });
let ring = ring_of(core::slice::from_ref(&original))?;
let decision = plan(&ring, None);
let [frame] = decision.pending() else {
return Err(format!("expected one frame, got {decision:?}").into());
};
let encoded = frame.encode();
assert_eq!(
encoded.matches("\ndata: ").count(),
1,
"the payload must occupy exactly one data line: {encoded:?}"
);
let decoded = SseEvent::decode(&encoded)?;
let recovered: Value = serde_json::from_str(decoded.data())?;
assert_eq!(recovered, original);
assert_eq!(
at(&recovered, &["text"]).and_then(Value::as_str),
Some("one\ntwo\nthree")
);
Ok(())
}
#[test]
fn a_payload_that_is_not_a_string_is_written_as_json() -> Result<(), TestError> {
let original = json!([1, "two", null, { "nested": true }]);
let ring = ring_of(core::slice::from_ref(&original))?;
let decision = plan(&ring, None);
let [frame] = decision.pending() else {
return Err(format!("expected one frame, got {decision:?}").into());
};
let data = frame.data();
assert!(data.starts_with('['), "Debug would print `Object`/`Array`");
assert!(!data.contains("Object"), "Debug leaked: {data}");
assert_eq!(serde_json::from_str::<Value>(data)?, original);
Ok(())
}
#[test]
fn a_delete_character_is_not_written_as_a_rust_escape() -> Result<(), TestError> {
let original = json!({ "t": "\u{7f}" });
let ring = ring_of(core::slice::from_ref(&original))?;
let decision = plan(&ring, None);
let [frame] = decision.pending() else {
return Err(format!("expected one frame, got {decision:?}").into());
};
let data = frame.data();
assert!(!data.contains("\\u{7f}"), "Debug escape leaked: {data}");
assert!(data.contains('\u{7f}'), "DEL should be written literally");
assert_eq!(serde_json::from_str::<Value>(data)?, original);
Ok(())
}
#[test]
fn the_mapping_agrees_with_the_whole_ring() -> Result<(), TestError> {
let ring = ring_of(&three_events())?;
for header in [None, Some("1"), Some("2"), Some("3"), Some("4")] {
let planned = plan(&ring, header);
let cursor = header.and_then(|raw| raw.parse::<u64>().ok());
let mapped = cursor.map_or_else(
|| decide(&ring.replay_from(None)),
|value| decide(&ring.replay_from(Some(EventId::new(value)))),
);
assert_eq!(planned, mapped, "disagreed on {header:?}");
}
Ok(())
}
}