use tokio::sync::broadcast;
pub const PIC_EVENT_QUEUE_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PicEventKind {
Claimed,
Released,
Disconnected,
}
impl PicEventKind {
pub fn as_str(self) -> &'static str {
match self {
PicEventKind::Claimed => "claimed",
PicEventKind::Released => "released",
PicEventKind::Disconnected => "disconnected",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PicEvent {
pub kind: PicEventKind,
pub client_id: Option<String>,
pub claim_counter: u64,
pub timestamp_ms: i64,
}
#[derive(Debug, Clone)]
pub struct PicEventBus {
tx: broadcast::Sender<PicEvent>,
}
impl PicEventBus {
pub fn new() -> Self {
Self::with_depth(PIC_EVENT_QUEUE_DEPTH)
}
pub fn with_depth(depth: usize) -> Self {
let (tx, _rx) = broadcast::channel(depth.max(1));
Self { tx }
}
pub fn subscribe(&self) -> broadcast::Receiver<PicEvent> {
self.tx.subscribe()
}
pub fn publish(&self, event: PicEvent) -> usize {
self.tx.send(event).unwrap_or(0)
}
pub fn receiver_count(&self) -> usize {
self.tx.receiver_count()
}
}
impl Default for PicEventBus {
fn default() -> Self {
Self::new()
}
}
pub const BUTTON_EVENT_QUEUE_DEPTH: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ButtonBusEvent {
pub button: u32,
pub label: String,
pub kind: &'static str,
pub action: Option<String>,
pub timestamp_ms: u64,
}
#[derive(Debug, Clone)]
pub struct ButtonEventBus {
tx: broadcast::Sender<ButtonBusEvent>,
}
impl ButtonEventBus {
pub fn new() -> Self {
Self::with_depth(BUTTON_EVENT_QUEUE_DEPTH)
}
pub fn with_depth(depth: usize) -> Self {
let (tx, _rx) = broadcast::channel(depth.max(1));
Self { tx }
}
pub fn subscribe(&self) -> broadcast::Receiver<ButtonBusEvent> {
self.tx.subscribe()
}
pub fn publish(&self, event: ButtonBusEvent) -> usize {
self.tx.send(event).unwrap_or(0)
}
pub fn receiver_count(&self) -> usize {
self.tx.receiver_count()
}
}
impl Default for ButtonEventBus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn publish_reaches_each_subscriber() {
let bus = PicEventBus::new();
let mut a = bus.subscribe();
let mut b = bus.subscribe();
let event = PicEvent {
kind: PicEventKind::Claimed,
client_id: Some("op-1".into()),
claim_counter: 1,
timestamp_ms: 42,
};
assert_eq!(bus.publish(event.clone()), 2);
assert_eq!(a.recv().await.unwrap(), event);
assert_eq!(b.recv().await.unwrap(), event);
}
#[test]
fn publish_with_no_subscribers_is_a_noop() {
let bus = PicEventBus::new();
let event = PicEvent {
kind: PicEventKind::Released,
client_id: None,
claim_counter: 0,
timestamp_ms: 0,
};
assert_eq!(bus.publish(event), 0);
}
#[tokio::test]
async fn slow_subscriber_is_lagged_not_blocking() {
let bus = PicEventBus::with_depth(2);
let mut slow = bus.subscribe();
for i in 0..5 {
bus.publish(PicEvent {
kind: PicEventKind::Claimed,
client_id: Some(format!("op-{i}")),
claim_counter: i,
timestamp_ms: i as i64,
});
}
match slow.recv().await {
Err(broadcast::error::RecvError::Lagged(n)) => assert!(n >= 1),
other => panic!("expected Lagged, got {other:?}"),
}
}
#[test]
fn kind_wire_strings_are_stable() {
assert_eq!(PicEventKind::Claimed.as_str(), "claimed");
assert_eq!(PicEventKind::Released.as_str(), "released");
assert_eq!(PicEventKind::Disconnected.as_str(), "disconnected");
}
#[tokio::test]
async fn button_bus_fans_out_to_each_subscriber() {
let bus = ButtonEventBus::new();
let mut a = bus.subscribe();
let mut b = bus.subscribe();
let event = ButtonBusEvent {
button: 5,
label: "B1".into(),
kind: "short",
action: Some("cycle_screen".into()),
timestamp_ms: 1500,
};
assert_eq!(bus.publish(event.clone()), 2);
assert_eq!(a.recv().await.unwrap(), event);
assert_eq!(b.recv().await.unwrap(), event);
}
#[test]
fn button_bus_publish_with_no_subscribers_is_a_noop() {
let bus = ButtonEventBus::new();
let event = ButtonBusEvent {
button: 6,
label: "B2".into(),
kind: "long",
action: None,
timestamp_ms: 2000,
};
assert_eq!(bus.publish(event), 0);
}
#[tokio::test]
async fn button_bus_slow_subscriber_is_lagged_not_blocking() {
let bus = ButtonEventBus::with_depth(2);
let mut slow = bus.subscribe();
for i in 0..5u32 {
bus.publish(ButtonBusEvent {
button: i,
label: format!("BX{i}"),
kind: "short",
action: None,
timestamp_ms: i as u64,
});
}
match slow.recv().await {
Err(broadcast::error::RecvError::Lagged(n)) => assert!(n >= 1),
other => panic!("expected Lagged, got {other:?}"),
}
}
}