use crate::node_id::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalResponse {
Continue,
Stop,
}
pub struct Signal<T: Clone + Send + 'static> {
subscribers: Vec<Subscription<T>>,
}
struct Subscription<T: Clone + Send + 'static> {
node: NodeId,
handler: fn(&T) -> SignalResponse,
}
impl<T: Clone + Send + 'static> Signal<T> {
pub fn new() -> Self {
Self {
subscribers: Vec::new(),
}
}
pub fn subscribe(&mut self, node: NodeId, handler: fn(&T) -> SignalResponse) {
self.subscribers.push(Subscription { node, handler });
}
pub fn unsubscribe(&mut self, node: NodeId) {
self.subscribers.retain(|s| s.node != node);
}
pub fn emit(&self, value: &T) -> usize {
let mut count = 0;
for sub in &self.subscribers {
count += 1;
if (sub.handler)(value) == SignalResponse::Stop {
break;
}
}
count
}
pub fn is_subscribed(&self, node: NodeId) -> bool {
self.subscribers.iter().any(|s| s.node == node)
}
pub fn subscriber_count(&self) -> usize {
self.subscribers.len()
}
}
impl<T: Clone + Send + 'static> Default for Signal<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::SlotMap;
use std::sync::atomic::{AtomicUsize, Ordering};
fn make_nodes(n: usize) -> (SlotMap<NodeId, &'static str>, Vec<NodeId>) {
let mut sm = SlotMap::new();
let ids: Vec<NodeId> = (0..n)
.map(|i| {
sm.insert(if i == 0 {
"a"
} else if i == 1 {
"b"
} else {
"c"
})
})
.collect();
(sm, ids)
}
#[test]
fn basic_subscribe_and_emit() {
let (_sm, ids) = make_nodes(1);
let mut sig = Signal::<u32>::new();
sig.subscribe(ids[0], |val| {
assert_eq!(*val, 42);
SignalResponse::Continue
});
assert_eq!(sig.emit(&42), 1);
}
#[test]
fn multiple_subscribers_all_receive_value() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let (_sm, ids) = make_nodes(3);
let mut sig = Signal::<u32>::new();
COUNTER.store(0, Ordering::SeqCst);
for &id in &ids {
sig.subscribe(id, |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
}
let notified = sig.emit(&99);
assert_eq!(notified, 3);
assert_eq!(COUNTER.load(Ordering::SeqCst), 3);
}
#[test]
fn subscribers_notified_in_order() {
use std::sync::Mutex;
static ORDER: Mutex<Vec<u32>> = Mutex::new(Vec::new());
let (_sm, ids) = make_nodes(3);
let mut sig = Signal::<u32>::new();
ORDER.lock().unwrap().clear();
sig.subscribe(ids[0], |_| {
ORDER.lock().unwrap().push(0);
SignalResponse::Continue
});
sig.subscribe(ids[1], |_| {
ORDER.lock().unwrap().push(1);
SignalResponse::Continue
});
sig.subscribe(ids[2], |_| {
ORDER.lock().unwrap().push(2);
SignalResponse::Continue
});
sig.emit(&0);
assert_eq!(*ORDER.lock().unwrap(), vec![0, 1, 2]);
}
#[test]
fn unsubscribe_removes_node() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let (_sm, ids) = make_nodes(2);
let mut sig = Signal::<u32>::new();
sig.subscribe(ids[0], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
sig.subscribe(ids[1], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
sig.unsubscribe(ids[0]);
COUNTER.store(0, Ordering::SeqCst);
let notified = sig.emit(&1);
assert_eq!(notified, 1);
assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
assert!(!sig.is_subscribed(ids[0]));
assert!(sig.is_subscribed(ids[1]));
}
#[test]
fn signal_response_stop_halts_remaining() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let (_sm, ids) = make_nodes(3);
let mut sig = Signal::<u32>::new();
COUNTER.store(0, Ordering::SeqCst);
sig.subscribe(ids[0], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
sig.subscribe(ids[1], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Stop });
sig.subscribe(ids[2], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
let notified = sig.emit(&0);
assert_eq!(notified, 2);
assert_eq!(COUNTER.load(Ordering::SeqCst), 2);
}
#[test]
fn emit_with_no_subscribers_returns_zero() {
let sig = Signal::<String>::new();
assert_eq!(sig.emit(&"hello".to_string()), 0);
}
#[test]
fn is_subscribed_before_and_after() {
let (_sm, ids) = make_nodes(1);
let mut sig = Signal::<u32>::new();
assert!(!sig.is_subscribed(ids[0]));
sig.subscribe(ids[0], |_| SignalResponse::Continue);
assert!(sig.is_subscribed(ids[0]));
sig.unsubscribe(ids[0]);
assert!(!sig.is_subscribed(ids[0]));
}
#[test]
fn subscriber_count_tracks_correctly() {
let (_sm, ids) = make_nodes(2);
let mut sig = Signal::<u32>::new();
assert_eq!(sig.subscriber_count(), 0);
sig.subscribe(ids[0], |_| SignalResponse::Continue);
assert_eq!(sig.subscriber_count(), 1);
sig.subscribe(ids[1], |_| SignalResponse::Continue);
assert_eq!(sig.subscriber_count(), 2);
sig.unsubscribe(ids[0]);
assert_eq!(sig.subscriber_count(), 1);
sig.unsubscribe(ids[1]);
assert_eq!(sig.subscriber_count(), 0);
}
#[test]
fn multiple_subscriptions_same_node_both_fire() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let (_sm, ids) = make_nodes(1);
let mut sig = Signal::<u32>::new();
COUNTER.store(0, Ordering::SeqCst);
sig.subscribe(ids[0], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
sig.subscribe(ids[0], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
assert_eq!(sig.subscriber_count(), 2);
let notified = sig.emit(&7);
assert_eq!(notified, 2);
assert_eq!(COUNTER.load(Ordering::SeqCst), 2);
sig.unsubscribe(ids[0]);
assert_eq!(sig.subscriber_count(), 0);
}
#[test]
fn default_trait_works() {
let sig: Signal<f64> = Signal::default();
assert_eq!(sig.subscriber_count(), 0);
assert_eq!(sig.emit(&3.14), 0);
}
#[test]
fn handler_receives_correct_value() {
let (_sm, ids) = make_nodes(1);
let mut sig = Signal::<String>::new();
sig.subscribe(ids[0], |val| {
assert_eq!(val, "expected");
SignalResponse::Continue
});
sig.emit(&"expected".to_string());
}
#[test]
fn stop_on_first_subscriber() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let (_sm, ids) = make_nodes(2);
let mut sig = Signal::<u32>::new();
COUNTER.store(0, Ordering::SeqCst);
sig.subscribe(ids[0], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Stop
});
sig.subscribe(ids[1], |_| {
COUNTER.fetch_add(1, Ordering::SeqCst);
SignalResponse::Continue
});
let notified = sig.emit(&0);
assert_eq!(notified, 1);
assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
}
#[test]
fn unsubscribe_nonexistent_node_is_noop() {
let (_sm, ids) = make_nodes(2);
let mut sig = Signal::<u32>::new();
sig.subscribe(ids[0], |_| SignalResponse::Continue);
sig.unsubscribe(ids[1]);
assert_eq!(sig.subscriber_count(), 1);
assert!(sig.is_subscribed(ids[0]));
}
}