use futures::channel::mpsc::Receiver;
use futures::stream::StreamExt;
use pipecrab_core::{DataFrame, Direction, SystemFrame};
#[derive(Debug)]
pub(crate) struct Stamped<T> {
pub(crate) seq: u64,
pub(crate) frame: T,
}
#[derive(Debug)]
pub enum Received {
Sys(Direction, SystemFrame),
Data(DataFrame),
}
pub struct Inbound {
pub(crate) sys: Receiver<Stamped<(Direction, SystemFrame)>>,
pub(crate) data: Receiver<Stamped<DataFrame>>,
pub(crate) flush_floor: u64,
}
impl Inbound {
pub async fn recv(&mut self) -> Option<Received> {
loop {
futures::select_biased! {
sys = self.sys.next() => {
if let Some(Stamped { seq, frame: (dir, f) }) = sys {
self.flush_floor = seq;
return Some(Received::Sys(dir, f));
}
}
data = self.data.next() => {
if let Some(Stamped { frame, .. }) = data {
return Some(Received::Data(frame));
}
}
complete => return None,
}
}
}
pub fn flush_data(&mut self) -> Vec<DataFrame> {
self.flush_data_stamped()
.into_iter()
.map(|stamped| stamped.frame)
.collect()
}
pub(crate) fn flush_data_stamped(&mut self) -> Vec<Stamped<DataFrame>> {
let mut kept = Vec::new();
while let Ok(stamped) = self.data.try_recv() {
if stamped.seq >= self.flush_floor || stamped.frame.survives_flush() {
kept.push(stamped);
}
}
kept
}
pub async fn recv_sys(&mut self) -> Option<(Direction, SystemFrame)> {
let Stamped {
seq,
frame: (dir, frame),
} = self.sys.next().await?;
self.flush_floor = seq;
Some((dir, frame))
}
pub(crate) fn try_recv_sys(&mut self) -> Option<(Direction, SystemFrame)> {
match self.sys.try_recv() {
Ok(Stamped {
seq,
frame: (dir, frame),
}) => {
self.flush_floor = seq;
Some((dir, frame))
}
Err(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use futures::FutureExt;
use futures::channel::mpsc;
use futures::executor::block_on;
use pipecrab_core::Transcript;
use super::*;
#[allow(clippy::type_complexity)]
fn lanes() -> (
mpsc::Sender<Stamped<(Direction, SystemFrame)>>,
mpsc::Sender<Stamped<DataFrame>>,
Inbound,
) {
let (sys_tx, sys) = mpsc::channel(16);
let (data_tx, data) = mpsc::channel(16);
(
sys_tx,
data_tx,
Inbound {
sys,
data,
flush_floor: 0,
},
)
}
#[test]
fn both_lanes_closed_yields_none() {
block_on(async {
let (sys_tx, data_tx, mut inb) = lanes();
drop(sys_tx);
drop(data_tx);
assert!(
inb.recv().await.is_none(),
"closed lanes must signal shutdown via None"
);
});
}
#[test]
fn one_closed_lane_does_not_signal_shutdown() {
block_on(async {
let (sys_tx, data_tx, mut inb) = lanes();
drop(data_tx);
assert!(
inb.recv().now_or_never().is_none(),
"a still-open sys lane must keep recv pending, not report shutdown",
);
drop(sys_tx);
assert!(
matches!(inb.recv().now_or_never(), Some(None)),
"both lanes closed must resolve immediately to None",
);
});
}
#[test]
fn closed_sys_lane_still_serves_buffered_data() {
block_on(async {
let (sys_tx, mut data_tx, mut inb) = lanes();
data_tx
.try_send(Stamped {
seq: 1,
frame: Transcript::user_final("after sys closed").into(),
})
.unwrap();
drop(sys_tx);
match inb.recv().await.unwrap() {
Received::Data(DataFrame::Transcript(s)) => {
assert_eq!(s.text, "after sys closed".into())
}
other => panic!("closed sys lane must not block the data lane, got {other:?}"),
}
});
}
#[test]
fn recv_sys_takes_the_system_frame_past_a_backed_up_data_lane() {
block_on(async {
let (mut sys_tx, mut data_tx, mut inb) = lanes();
data_tx
.try_send(Stamped {
seq: 1,
frame: Transcript::user_final("stale").into(),
})
.unwrap();
sys_tx
.try_send(Stamped {
seq: 2,
frame: (Direction::Down, SystemFrame::Interrupt),
})
.unwrap();
data_tx
.try_send(Stamped {
seq: 3,
frame: Transcript::user_final("barge-in").into(),
})
.unwrap();
assert!(
matches!(
inb.recv_sys().await,
Some((Direction::Down, SystemFrame::Interrupt))
),
"recv_sys must reach the system frame without draining data first",
);
let kept = inb.flush_data();
assert_eq!(kept.len(), 1, "only the post-Interrupt frame survives");
match &kept[0] {
DataFrame::Transcript(s) => assert_eq!(s.text, "barge-in".into()),
other => panic!("wrong survivor: {other:?}"),
}
});
}
#[test]
fn recv_sys_reports_a_closed_sys_lane_and_leaves_data_alone() {
block_on(async {
let (sys_tx, mut data_tx, mut inb) = lanes();
data_tx
.try_send(Stamped {
seq: 1,
frame: Transcript::user_final("still here").into(),
})
.unwrap();
drop(sys_tx);
assert!(
matches!(inb.recv_sys().now_or_never(), Some(None)),
"a closed sys lane must resolve immediately, so a racing caller can stop polling",
);
match inb.recv().await.unwrap() {
Received::Data(DataFrame::Transcript(s)) => {
assert_eq!(s.text, "still here".into())
}
other => panic!("recv_sys must not consume data frames, got {other:?}"),
}
});
}
}