use crate::{MessageStreamScanner, TransportError};
pub const REALTIME_TAG_DELTA: u8 = 0x00;
pub const REALTIME_TAG_ROUND: u8 = 0x01;
#[derive(Debug)]
pub enum RoundInbound {
Delta(Vec<u8>),
RoundProgress,
RoundComplete(Vec<u8>),
Ignored,
}
#[derive(Default)]
pub struct RealtimeRound {
scanner: Option<MessageStreamScanner>,
}
impl RealtimeRound {
pub fn new() -> Self {
Self::default()
}
pub fn in_flight(&self) -> bool {
self.scanner.is_some()
}
pub fn begin(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
if self.scanner.is_some() {
return Err(TransportError::new(
"sync.transport_failed",
"a realtime sync round is already in flight (§8.7 one round per connection)",
));
}
self.scanner = Some(MessageStreamScanner::new());
let mut framed = Vec::with_capacity(request.len() + 1);
framed.push(REALTIME_TAG_ROUND);
framed.extend_from_slice(request);
Ok(framed)
}
pub fn route_binary(&mut self, frame: &[u8]) -> Result<RoundInbound, TransportError> {
if frame.is_empty() {
return Ok(RoundInbound::Ignored);
}
let tag = frame[0];
let body = &frame[1..];
match tag {
REALTIME_TAG_ROUND => {
let Some(scanner) = self.scanner.as_mut() else {
return Ok(RoundInbound::Ignored);
};
match scanner.push(body) {
Ok(None) => Ok(RoundInbound::RoundProgress),
Ok(Some(done)) => {
self.scanner = None;
if done.excess > 0 {
return Err(TransportError::new(
"sync.transport_failed",
"realtime round response has bytes past END (§8.7)",
));
}
Ok(RoundInbound::RoundComplete(done.message))
}
Err(error) => {
self.scanner = None;
Err(TransportError::new(
"sync.transport_failed",
format!("realtime round response decode error: {}", error.detail),
))
}
}
}
REALTIME_TAG_DELTA => Ok(RoundInbound::Delta(body.to_vec())),
_ => Ok(RoundInbound::Ignored),
}
}
pub fn abort(&mut self) {
self.scanner = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use ssp2::model::{Frame, Message, MsgKind};
use ssp2::{encode_message, MessageStreamScanner as _Scanner};
fn response_bytes() -> Vec<u8> {
let message = Message {
msg_kind: MsgKind::Response,
frames: vec![Frame::RespHeader {
required_schema_version: None,
latest_schema_version: None,
}],
};
encode_message(&message)
}
fn tagged(tag: u8, body: &[u8]) -> Vec<u8> {
let mut v = vec![tag];
v.extend_from_slice(body);
v
}
#[test]
fn begin_frames_request_with_round_tag_and_marks_in_flight() {
let mut round = RealtimeRound::new();
assert!(!round.in_flight());
let framed = round.begin(&[0xde, 0xad]).unwrap();
assert_eq!(framed, vec![REALTIME_TAG_ROUND, 0xde, 0xad]);
assert!(round.in_flight());
}
#[test]
fn second_begin_while_in_flight_is_rejected() {
let mut round = RealtimeRound::new();
round.begin(&[0x01]).unwrap();
let err = round.begin(&[0x02]).unwrap_err();
assert_eq!(err.code, "sync.transport_failed");
assert!(err.message.contains("one round"));
}
#[test]
fn single_chunk_response_completes_and_clears_in_flight() {
let response = response_bytes();
let mut round = RealtimeRound::new();
round.begin(&[0x00]).unwrap();
let frame = tagged(REALTIME_TAG_ROUND, &response);
match round.route_binary(&frame).unwrap() {
RoundInbound::RoundComplete(bytes) => assert_eq!(bytes, response),
_ => panic!("expected RoundComplete"),
}
assert!(!round.in_flight(), "round clears after END");
}
#[test]
fn chunked_response_reassembles_across_arbitrary_boundaries() {
let response = response_bytes();
for split in 1..response.len() {
let mut round = RealtimeRound::new();
round.begin(&[0x00]).unwrap();
let first = tagged(REALTIME_TAG_ROUND, &response[..split]);
assert!(matches!(
round.route_binary(&first).unwrap(),
RoundInbound::RoundProgress
));
let second = tagged(REALTIME_TAG_ROUND, &response[split..]);
match round.route_binary(&second).unwrap() {
RoundInbound::RoundComplete(bytes) => {
assert_eq!(bytes, response, "split {split}")
}
_ => panic!("split {split}: expected RoundComplete"),
}
}
}
#[test]
fn delta_during_round_is_queued_not_applied_to_round() {
let response = response_bytes();
let mut round = RealtimeRound::new();
round.begin(&[0x00]).unwrap();
let delta = tagged(REALTIME_TAG_DELTA, &[0xaa, 0xbb]);
match round.route_binary(&delta).unwrap() {
RoundInbound::Delta(body) => assert_eq!(body, vec![0xaa, 0xbb]),
_ => panic!("expected Delta"),
}
assert!(round.in_flight(), "delta does not end the round");
let frame = tagged(REALTIME_TAG_ROUND, &response);
assert!(matches!(
round.route_binary(&frame).unwrap(),
RoundInbound::RoundComplete(_)
));
}
#[test]
fn bytes_past_end_fail_the_round() {
let mut response = response_bytes();
response.extend_from_slice(&[0xff, 0xff]);
let mut round = RealtimeRound::new();
round.begin(&[0x00]).unwrap();
let frame = tagged(REALTIME_TAG_ROUND, &response);
let err = round.route_binary(&frame).unwrap_err();
assert_eq!(err.code, "sync.transport_failed");
assert!(err.message.contains("past END"));
assert!(!round.in_flight());
}
#[test]
fn round_chunk_with_no_round_in_flight_is_ignored() {
let mut round = RealtimeRound::new();
let frame = tagged(REALTIME_TAG_ROUND, &[0x01, 0x02]);
assert!(matches!(
round.route_binary(&frame).unwrap(),
RoundInbound::Ignored
));
}
#[test]
fn unknown_tag_is_ignored() {
let mut round = RealtimeRound::new();
round.begin(&[0x00]).unwrap();
let frame = tagged(0x7f, &[0x01]);
assert!(matches!(
round.route_binary(&frame).unwrap(),
RoundInbound::Ignored
));
assert!(round.in_flight());
}
#[allow(dead_code)]
fn _uses_scanner(_: _Scanner) {}
}