use crate::reactive::error::WriteResult;
use crate::reactive::watch::{Immediate, Watch, Watchable};
use amethystate_core::{Signal, SignalSubscription};
use std::sync::Arc;
use uuid::Uuid;
type Writer<T> = Arc<dyn Fn(T) -> WriteResult<()> + Send + Sync>;
pub struct ReactiveCell<T> {
cache: Signal<T>,
writer: Writer<T>,
origin: Uuid,
_keepalive: Option<Arc<dyn Send + Sync>>,
}
impl<T> Clone for ReactiveCell<T> {
fn clone(&self) -> Self {
Self {
cache: self.cache.clone(),
writer: Arc::clone(&self.writer),
origin: self.origin,
_keepalive: self._keepalive.clone(),
}
}
}
impl<T> ReactiveCell<T>
where
T: Clone + Send + Sync + 'static,
{
pub(crate) fn from_parts(
cache: Signal<T>,
writer: Writer<T>,
origin: Uuid,
keepalive: Option<Arc<dyn Send + Sync>>,
) -> Self {
Self {
cache,
writer,
origin,
_keepalive: keepalive,
}
}
pub fn new(initial: T) -> Self {
let cache = Signal::new(initial);
let sink = cache.clone();
Self {
cache,
writer: Arc::new(move |value| {
sink.set(value);
Ok(())
}),
origin: Uuid::new_v4(),
_keepalive: None,
}
}
pub fn get(&self) -> T {
self.cache.get()
}
pub fn set(&self, value: T) -> WriteResult<()> {
(self.writer)(value)
}
pub fn update<F>(&self, f: F) -> WriteResult<T>
where
F: FnOnce(T) -> T,
{
let next = f(self.get());
self.set(next.clone())?;
Ok(next)
}
pub fn modify<F>(&self, f: F) -> WriteResult<()>
where
F: FnOnce(&mut T),
{
let mut value = self.get();
f(&mut value);
self.set(value)
}
#[track_caller]
pub fn subscribe<F>(&self, callback: F) -> SignalSubscription
where
F: Fn(&T) + Send + Sync + 'static,
{
self.cache.subscribe(callback)
}
#[track_caller]
pub fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
where
F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
{
self.cache.subscribe_with_source(callback)
}
pub fn subscription_with(&self) -> Watch<Self, Immediate> {
Watch::new(self.clone())
}
}
impl<T> Watchable for ReactiveCell<T>
where
T: Clone + Send + Sync + 'static,
{
type Item = T;
fn watch_id(&self) -> Uuid {
self.origin
}
fn watch_raw<F>(&self, callback: F) -> SignalSubscription
where
F: Fn(&T, Option<Uuid>) + Send + Sync + 'static,
{
self.cache.subscribe_with_source(callback)
}
}
impl<T> amethystate_core::pipeline::Reactive<T> for ReactiveCell<T>
where
T: Clone + Send + Sync + 'static,
{
fn get(&self) -> T {
self.get()
}
fn subscribe_with_source<F>(&self, callback: F) -> SignalSubscription
where
F: for<'a> Fn(&'a T, Option<Uuid>) + Send + Sync + 'static,
{
self.cache.subscribe_with_source(callback)
}
fn keepalive(&self) -> Option<Arc<dyn Send + Sync>> {
self._keepalive.clone()
}
}
impl<T: std::fmt::Debug + Clone + Send + Sync + 'static> std::fmt::Debug for ReactiveCell<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReactiveCell")
.field("value", &self.get())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DefaultStore;
use crate::store::{StateScope, Store};
use crate::test_utils::unique_store;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
struct UiScope;
impl StateScope for UiScope {
const PREFIX: &'static str = "ui";
}
fn stored_field(
store: &DefaultStore,
name: &'static str,
default: u64,
) -> crate::WritableField<u64, DefaultStore> {
crate::store::field::<UiScope, u64, DefaultStore>(store, name, default, Uuid::new_v4())
.expect("field")
}
#[test]
fn volatile_cell_round_trips() {
let cell = ReactiveCell::new(7u64);
assert_eq!(cell.get(), 7);
cell.set(9).expect("volatile write cannot fail");
assert_eq!(cell.get(), 9);
}
#[test]
fn field_cell_writes_reach_the_store() {
let store = unique_store("write-through");
let field = stored_field(&store, "width", 110);
let cell = field.cell();
cell.set(200).expect("write should reach the store");
assert_eq!(store.get::<u64>("ui.width").unwrap(), Some(200));
assert_eq!(cell.get(), 200, "cache must reflect the committed value");
assert_eq!(field.get(), 200, "field and cell share one cache");
}
#[test]
fn cell_outlives_the_field_it_came_from() {
let store = unique_store("outlive");
let cell = {
let field = stored_field(&store, "height", 10);
field.cell()
};
store.set("ui.height", &42u64).expect("external write");
assert_eq!(
cell.get(),
42,
"cache must still be fed after the field is dropped"
);
}
#[test]
fn subscribers_see_writes_from_anywhere() {
let store = unique_store("subscribe");
let field = stored_field(&store, "depth", 1);
let cell = field.cell();
let seen = Arc::new(Mutex::new(Vec::new()));
let cap = seen.clone();
let _sub = cell.subscribe(move |v: &u64| cap.lock().unwrap().push(*v));
cell.set(2).unwrap();
field.set(3).unwrap();
store.set("ui.depth", &4u64).unwrap();
assert_eq!(*seen.lock().unwrap(), vec![2, 3, 4]);
}
#[test]
fn write_through_cell_fires_once_per_write() {
let store = unique_store("single-fire");
let cell = stored_field(&store, "fires", 0).cell();
let fires = Arc::new(AtomicUsize::new(0));
let cap = fires.clone();
let _sub = cell.subscribe(move |_: &u64| {
cap.fetch_add(1, Ordering::SeqCst);
});
cell.set(1).unwrap();
assert_eq!(fires.load(Ordering::SeqCst), 1, "one write, one fire");
}
#[test]
fn rejected_write_reports_and_leaves_the_cache_alone() {
let store = unique_store("rejected");
let field = stored_field(&store, "guarded", 5);
let _guard = field.intercept(|_change| None);
let cell = field.cell();
let result = cell.set(99);
assert!(result.is_err(), "rejected write must not report success");
assert_eq!(
cell.get(),
5,
"cache must not hold a value the store refused"
);
assert_eq!(store.get::<u64>("ui.guarded").unwrap(), Some(5));
}
#[test]
fn clones_share_one_value() {
let cell = ReactiveCell::new(1u64);
let twin = cell.clone();
twin.set(2).unwrap();
assert_eq!(cell.get(), 2, "clones are handles onto the same cell");
}
}