use std::cell::Ref;
use std::rc::{Rc, Weak};
use crate::feature::Dispatch;
use crate::scope::{DropGuard, Reducer, Scope, Slot, Subscription};
pub struct ReducerBinding<R: Reducer> {
owner: Weak<Scope>,
state: Rc<Slot<R>>,
dispatch: Dispatch,
}
impl<R: Reducer> Clone for ReducerBinding<R> {
fn clone(&self) -> Self {
Self {
owner: self.owner.clone(),
state: self.state.clone(),
dispatch: self.dispatch.clone(),
}
}
}
impl<R: Reducer> ReducerBinding<R> {
pub(crate) fn new(scope: &Rc<Scope>) -> Self {
Self {
owner: Rc::downgrade(scope),
state: scope.state::<R>(),
dispatch: Dispatch::owning::<R>(scope),
}
}
pub fn peek(&self) -> Ref<'_, R> {
Ref::map(self.state.borrow(), |state| &**state)
}
pub fn get(&self) -> Rc<R> {
self.state.borrow().clone()
}
pub fn dispatch(&self) -> Dispatch {
self.dispatch.clone()
}
pub fn push(&self, update: R::Update) {
if let Some(scope) = self.owner.upgrade() {
scope.push::<R>(update);
}
}
pub fn on_change(&self, f: impl Fn(&R) + 'static) -> Subscription {
let Some(scope) = self.owner.upgrade() else {
return Subscription::inert();
};
let state = self.state.clone();
scope.subscribe::<R>(move || {
let now = state.borrow().clone();
f(&now)
})
}
pub fn on_change_owned(&self, f: impl Fn(&R) + 'static) {
let Some(scope) = self.owner.upgrade() else {
return;
};
let subscription = self.on_change(f);
scope.own(DropGuard(subscription));
}
pub fn bind(&self, f: impl Fn(&R) + 'static) -> Subscription {
f(&self.get());
self.on_change(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[derive(Clone, Default, Debug)]
struct Counter(u32);
impl Reducer for Counter {
type Update = u32;
fn reduce(&mut self, by: u32) {
self.0 += by;
}
}
#[test]
fn a_binding_does_not_keep_its_scope_alive() {
let scope = Rc::new(Scope::new());
let binding = scope.binding::<Counter>();
assert_eq!(Rc::strong_count(&scope), 1, "the binding holds a Weak");
binding.on_change_owned(|_| {});
drop(scope);
assert_eq!(binding.peek().0, 0, "the state cell outlives the scope");
binding.push(1);
assert_eq!(binding.peek().0, 0, "pushing into a dead scope is a no-op");
}
#[test]
fn subscribing_does_not_keep_the_scope_alive_either() {
let scope = Rc::new(Scope::new());
let _subscription = scope.binding::<Counter>().on_change(|_| {});
assert_eq!(
Rc::strong_count(&scope),
1,
"an outstanding subscription must not raise the scope's strong count"
);
}
#[test]
fn dropping_a_subscription_after_its_scope_is_harmless() {
let subscription = {
let scope = Rc::new(Scope::new());
scope.binding::<Counter>().on_change(|_| {})
};
drop(subscription);
}
#[test]
fn on_change_sees_every_push_until_dropped() {
let scope = Rc::new(Scope::new());
let binding = scope.binding::<Counter>();
let seen = Rc::new(Cell::new(0u32));
let recorder = seen.clone();
let subscription = binding.on_change(move |counter| recorder.set(counter.0));
binding.push(2);
binding.push(3);
assert_eq!(seen.get(), 5);
drop(subscription);
binding.push(4);
assert_eq!(seen.get(), 5, "no delivery after the subscription is dropped");
}
#[test]
fn bind_calls_back_before_anything_changes() {
let scope = Rc::new(Scope::new());
let binding = scope.binding::<Counter>();
binding.push(7);
let seen = Rc::new(Cell::new(None));
let recorder = seen.clone();
let _subscription = binding.bind(move |counter| recorder.set(Some(counter.0)));
assert_eq!(seen.get(), Some(7));
}
#[test]
fn a_read_is_shared_and_a_change_while_it_is_held_goes_to_a_copy() {
let scope = Rc::new(Scope::new());
let binding = scope.binding::<Counter>();
binding.push(1);
let first = binding.get();
let again = binding.get();
assert!(Rc::ptr_eq(&first, &again), "reading twice copies nothing");
binding.push(2);
assert_eq!(first.0, 1, "what was read stays as it was");
assert_eq!(binding.get().0, 3);
drop((first, again));
let before = Rc::as_ptr(&binding.get());
binding.push(4);
assert_eq!(Rc::as_ptr(&binding.get()), before, "nobody held it, so no copy");
}
}