use crate::errors::QlResult;
use crate::patterns::observable::Observable;
use crate::shared::{Shared, SharedMut, shared};
use super::observable::Observer;
struct UpdatingGuard<'a>(&'a mut bool);
impl Drop for UpdatingGuard<'_> {
fn drop(&mut self) {
*self.0 = false;
}
}
pub struct LazyObject {
observable: Shared<Observable>,
calculated: bool,
frozen: bool,
failed: bool,
always_forward: bool,
updating: bool,
}
pub(crate) struct DeferredUpdate {
lazy: SharedMut<LazyObject>,
observable: Shared<Observable>,
}
impl DeferredUpdate {
pub(crate) fn notify_observers(&self) {
self.observable.notify_observers();
}
}
impl Drop for DeferredUpdate {
fn drop(&mut self) {
self.lazy.borrow_mut().updating = false;
}
}
impl LazyObject {
pub fn new(always_forward: bool) -> Self {
LazyObject {
observable: shared(Observable::new()),
calculated: false,
frozen: false,
failed: false,
always_forward,
updating: false,
}
}
pub fn observable(&self) -> &Observable {
&self.observable
}
pub fn observable_handle(&self) -> Shared<Observable> {
Shared::clone(&self.observable)
}
pub fn is_calculated(&self) -> bool {
self.calculated
}
pub fn forward_first_notification_only(&mut self) {
self.always_forward = false;
}
pub fn always_forward_notifications(&mut self) {
self.always_forward = true;
}
pub fn on_update(&mut self) -> bool {
if self.updating {
return false;
}
self.updating = true;
let _reset = UpdatingGuard(&mut self.updating);
let mut notified = false;
if self.calculated || self.failed || self.always_forward {
self.calculated = false;
self.failed = false;
if !self.frozen {
self.observable.notify_observers();
notified = true;
}
}
notified
}
pub(crate) fn deferred_update(lazy: &SharedMut<LazyObject>) -> Option<DeferredUpdate> {
let observable = {
let mut lazy = lazy.borrow_mut();
if lazy.updating || !(lazy.calculated || lazy.failed || lazy.always_forward) {
return None;
}
lazy.updating = true;
lazy.calculated = false;
lazy.failed = false;
if lazy.frozen {
lazy.updating = false;
return None;
}
Shared::clone(&lazy.observable)
};
Some(DeferredUpdate {
lazy: SharedMut::clone(lazy),
observable,
})
}
pub fn mark_calculated(&mut self) {
self.calculated = true;
self.failed = false;
}
pub fn invalidate_silently(&mut self) {
self.calculated = false;
self.failed = false;
}
pub fn start_calculation(&mut self) -> bool {
if self.calculated || self.frozen {
return false;
}
self.calculated = true;
true
}
pub fn finish_calculation(&mut self, result: &QlResult<()>) {
match result {
Ok(()) => self.failed = false,
Err(_) => {
self.calculated = false;
self.failed = true;
}
}
}
pub fn calculate(&mut self, perform: impl FnOnce() -> QlResult<()>) -> QlResult<()> {
if !self.start_calculation() {
return Ok(());
}
let result = perform();
self.finish_calculation(&result);
result
}
pub fn recalculate(&mut self, perform: impl FnOnce() -> QlResult<()>) -> QlResult<()> {
let was_frozen = self.start_recalculation();
let result = self.calculate(perform);
self.finish_recalculation(was_frozen);
self.observable.notify_observers();
result
}
pub fn start_recalculation(&mut self) -> bool {
let was_frozen = self.frozen;
self.calculated = false;
self.frozen = false;
self.failed = false;
was_frozen
}
pub fn finish_recalculation(&mut self, was_frozen: bool) {
self.frozen = was_frozen;
}
pub fn freeze(&mut self) {
self.frozen = true;
}
pub fn unfreeze(&mut self) {
if let Some(observable) = self.deferred_unfreeze() {
observable.notify_observers();
}
}
pub fn deferred_unfreeze(&mut self) -> Option<Shared<Observable>> {
if self.frozen {
self.frozen = false;
Some(Shared::clone(&self.observable))
} else {
None
}
}
pub fn register_observer(&self, observer: &SharedMut<dyn Observer>) -> bool {
self.observable.register_observer(observer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fail;
use crate::shared::shared_mut;
#[derive(Default)]
struct Flag {
up: bool,
}
impl Flag {
fn new() -> SharedMut<Flag> {
shared_mut(Flag::default())
}
}
impl Observer for Flag {
fn update(&mut self) {
self.up = true;
}
}
struct Lazy {
lazy: LazyObject,
fail: bool,
calc_count: usize,
}
impl Lazy {
fn new(always_forward: bool) -> Self {
Lazy {
lazy: LazyObject::new(always_forward),
fail: false,
calc_count: 0,
}
}
fn npv(&mut self) -> QlResult<()> {
let count = &mut self.calc_count;
let fail = self.fail;
self.lazy.calculate(|| {
*count += 1;
if fail {
fail!("intentional failure");
}
Ok(())
})
}
fn force_npv(&mut self) -> QlResult<()> {
let count = &mut self.calc_count;
let fail = self.fail;
self.lazy.recalculate(|| {
*count += 1;
if fail {
fail!("intentional failure");
}
Ok(())
})
}
fn on_input_change(&mut self) {
self.lazy.on_update();
}
}
#[test]
fn calculate_runs_once_until_invalidated() {
let mut s = Lazy::new(true);
s.npv().unwrap();
s.npv().unwrap();
assert_eq!(s.calc_count, 1);
s.on_input_change();
s.npv().unwrap();
assert_eq!(s.calc_count, 2);
}
#[test]
fn forward_first_notification_only() {
let mut s = Lazy::new(false);
let flag = Flag::new();
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.npv().unwrap();
s.on_input_change();
assert!(flag.borrow().up, "first change should be forwarded");
flag.borrow_mut().up = false;
s.on_input_change();
assert!(
!flag.borrow().up,
"second change without recalculation should be discarded"
);
flag.borrow_mut().up = false;
s.npv().unwrap();
s.on_input_change();
assert!(flag.borrow().up, "change after recalculation is forwarded");
}
#[test]
fn always_forward_notifications() {
let mut s = Lazy::new(true);
let flag = Flag::new();
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.npv().unwrap();
s.on_input_change();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
s.on_input_change();
assert!(flag.borrow().up, "every change should be forwarded");
}
#[test]
fn notification_after_failed_calculation() {
let mut s = Lazy::new(false);
let flag = Flag::new();
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.npv().unwrap();
s.on_input_change();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
s.fail = true;
assert!(s.npv().is_err());
assert!(!flag.borrow().up);
s.fail = false;
s.on_input_change();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
s.npv().unwrap();
assert!(!flag.borrow().up);
s.on_input_change();
assert!(flag.borrow().up);
flag.borrow_mut().up = false;
s.on_input_change();
assert!(!flag.borrow().up);
}
#[test]
fn recalculate_notifies_even_when_perform_fails() {
let mut s = Lazy::new(false);
let flag = Flag::new();
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.fail = true;
assert!(s.force_npv().is_err());
assert!(
flag.borrow().up,
"failed recalculate must still notify observers"
);
flag.borrow_mut().up = false;
s.fail = false;
s.force_npv().unwrap();
assert!(
flag.borrow().up,
"successful recalculate notifies observers"
);
}
#[test]
fn recursive_update_is_broken_by_guard() {
let mut s = Lazy::new(true);
s.npv().unwrap();
s.on_input_change();
s.on_input_change();
assert!(!s.lazy.is_calculated());
}
#[test]
fn panicking_observer_does_not_latch_the_update_guard() {
use std::panic::{AssertUnwindSafe, catch_unwind};
struct Panicker {
armed: bool,
}
impl Observer for Panicker {
fn update(&mut self) {
if self.armed {
self.armed = false;
panic!("observer panic during notification");
}
}
}
let mut s = Lazy::new(true);
let flag = Flag::new();
let panicker = shared_mut(Panicker { armed: true });
s.lazy
.register_observer(&(panicker.clone() as SharedMut<dyn Observer>));
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.npv().unwrap();
let panicked = catch_unwind(AssertUnwindSafe(|| s.on_input_change()));
assert!(panicked.is_err(), "the observer panic should propagate");
flag.borrow_mut().up = false;
assert!(
s.lazy.on_update(),
"a panicking observer must not latch the update guard"
);
assert!(
flag.borrow().up,
"observers are notified again after recovery"
);
}
#[test]
fn freeze_suppresses_notifications() {
let mut s = Lazy::new(true);
let flag = Flag::new();
s.lazy
.register_observer(&(flag.clone() as SharedMut<dyn Observer>));
s.npv().unwrap();
s.lazy.freeze();
s.on_input_change();
assert!(!flag.borrow().up, "frozen object should not notify");
s.lazy.unfreeze();
assert!(flag.borrow().up, "unfreeze sends a catch-up notification");
}
}