#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ReactiveConfig {
pub initial: f32,
pub dead_zone: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ChangeListener {
pub id: u32,
pub tag: String,
pub last_seen: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ReactiveValue {
config: ReactiveConfig,
value: f32,
listeners: Vec<ChangeListener>,
next_id: u32,
notify_count: u32,
}
#[allow(dead_code)]
pub fn default_reactive_config() -> ReactiveConfig {
ReactiveConfig {
initial: 0.0,
dead_zone: 1e-6,
}
}
#[allow(dead_code)]
pub fn new_reactive_value(config: ReactiveConfig) -> ReactiveValue {
let initial = config.initial;
ReactiveValue {
config,
value: initial,
listeners: Vec::new(),
next_id: 1,
notify_count: 0,
}
}
#[allow(dead_code)]
pub fn reactive_get(rv: &ReactiveValue) -> f32 {
rv.value
}
#[allow(dead_code)]
pub fn reactive_set(rv: &mut ReactiveValue, new_val: f32) -> bool {
if (new_val - rv.value).abs() <= rv.config.dead_zone {
return false;
}
rv.value = new_val;
rv.notify_count += 1;
for l in &mut rv.listeners {
l.last_seen = new_val;
}
true
}
#[allow(dead_code)]
pub fn reactive_subscribe(rv: &mut ReactiveValue, tag: &str) -> u32 {
let id = rv.next_id;
rv.next_id += 1;
rv.listeners.push(ChangeListener {
id,
tag: tag.to_owned(),
last_seen: rv.value,
});
id
}
#[allow(dead_code)]
pub fn reactive_unsubscribe(rv: &mut ReactiveValue, id: u32) -> bool {
let before = rv.listeners.len();
rv.listeners.retain(|l| l.id != id);
rv.listeners.len() < before
}
#[allow(dead_code)]
pub fn reactive_listener_count(rv: &ReactiveValue) -> usize {
rv.listeners.len()
}
#[allow(dead_code)]
pub fn reactive_notify_count(rv: &ReactiveValue) -> u32 {
rv.notify_count
}
#[allow(dead_code)]
pub fn reactive_reset(rv: &mut ReactiveValue) {
rv.value = rv.config.initial;
rv.notify_count = 0;
for l in &mut rv.listeners {
l.last_seen = rv.value;
}
}
#[allow(dead_code)]
pub fn reactive_to_json(rv: &ReactiveValue) -> String {
format!(
r#"{{"value":{:.6},"listener_count":{},"notify_count":{}}}"#,
rv.value,
rv.listeners.len(),
rv.notify_count,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rv() -> ReactiveValue {
new_reactive_value(default_reactive_config())
}
#[test]
fn test_initial_value() {
let rv = make_rv();
assert!((reactive_get(&rv) - 0.0).abs() < 1e-9);
}
#[test]
fn test_set_triggers_notify() {
let mut rv = make_rv();
assert!(reactive_set(&mut rv, 1.0));
assert_eq!(reactive_notify_count(&rv), 1);
}
#[test]
fn test_set_within_dead_zone_no_notify() {
let mut rv = make_rv();
assert!(!reactive_set(&mut rv, 1e-9)); assert_eq!(reactive_notify_count(&rv), 0);
}
#[test]
fn test_subscribe_returns_id() {
let mut rv = make_rv();
let id = reactive_subscribe(&mut rv, "test");
assert!(id > 0);
assert_eq!(reactive_listener_count(&rv), 1);
}
#[test]
fn test_unsubscribe_removes_listener() {
let mut rv = make_rv();
let id = reactive_subscribe(&mut rv, "listener_a");
assert!(reactive_unsubscribe(&mut rv, id));
assert_eq!(reactive_listener_count(&rv), 0);
}
#[test]
fn test_unsubscribe_unknown_returns_false() {
let mut rv = make_rv();
assert!(!reactive_unsubscribe(&mut rv, 999));
}
#[test]
fn test_listener_last_seen_updated_on_set() {
let mut rv = make_rv();
reactive_subscribe(&mut rv, "a");
reactive_set(&mut rv, 0.7);
assert!((rv.listeners[0].last_seen - 0.7).abs() < 1e-6);
}
#[test]
fn test_reset_restores_initial() {
let mut rv = make_rv();
reactive_set(&mut rv, 5.0);
reactive_reset(&mut rv);
assert!((reactive_get(&rv) - 0.0).abs() < 1e-9);
assert_eq!(reactive_notify_count(&rv), 0);
}
#[test]
fn test_to_json_fields() {
let rv = make_rv();
let json = reactive_to_json(&rv);
assert!(json.contains("value"));
assert!(json.contains("listener_count"));
assert!(json.contains("notify_count"));
}
#[test]
fn test_multiple_listeners() {
let mut rv = make_rv();
reactive_subscribe(&mut rv, "a");
reactive_subscribe(&mut rv, "b");
reactive_subscribe(&mut rv, "c");
assert_eq!(reactive_listener_count(&rv), 3);
reactive_set(&mut rv, 2.0);
for l in &rv.listeners {
assert!((l.last_seen - 2.0).abs() < 1e-6);
}
}
}