Skip to content

Commit 44d7b60

Browse files
committed
feat(cache): Refactored the cache mechanism and optimized dependency management, aiming to eliminate tight coupling with global variables.
- Optimized dependency management using Rc and Weak - Rewrote the Memo structure and related logic
1 parent 2706c91 commit 44d7b60

File tree

8 files changed

+137
-88
lines changed

8 files changed

+137
-88
lines changed

cache/src/cache.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ use lru::LruCache;
44
use once_cell::unsync::Lazy;
55
use std::{any::Any, num::NonZeroUsize, rc::Rc};
66

7-
use crate::Observable;
8-
9-
type CacheKey = *const dyn Observable;
7+
use crate::IMemo;
108

119
const CACHE_CAP: usize = 128;
1210

13-
static mut CACHE: Lazy<LruCache<CacheKey, Rc<dyn Any>>> =
11+
static mut CACHE: Lazy<LruCache<*const dyn IMemo, Rc<dyn Any>>> =
1412
Lazy::new(|| LruCache::new(NonZeroUsize::new(CACHE_CAP).unwrap()));
1513

16-
pub(crate) fn touch<T>(key: &'static dyn Observable) -> Option<Rc<T>>
14+
pub(crate) fn touch<T>(key: &Rc<dyn IMemo>) -> Option<Rc<T>>
1715
where
1816
T: 'static,
1917
{
@@ -25,21 +23,21 @@ where
2523
return None;
2624
}
2725

28-
unsafe { CACHE.get(&(key as _)) }
26+
unsafe { CACHE.get(&Rc::as_ptr(key)) }
2927
.map(Rc::clone)
3028
.filter(|rc| rc.is::<T>())
3129
.map(|rc| unsafe { Rc::from_raw(Rc::into_raw(rc) as *const T) })
3230
}
3331

34-
pub(crate) fn store_in_cache<T>(key: &'static dyn Observable, val: T) -> Rc<T>
32+
pub(crate) fn store_in_cache<T>(key: &Rc<dyn IMemo>, val: T) -> Rc<T>
3533
where
3634
T: 'static,
3735
{
3836
let rc = Rc::new(val);
39-
unsafe { CACHE.put(key, Rc::clone(&rc) as _) };
37+
unsafe { CACHE.put(Rc::as_ptr(key), Rc::clone(&rc) as _) };
4038
rc
4139
}
4240

43-
pub(crate) fn remove_from_cache(key: &'static dyn Observable) {
44-
unsafe { CACHE.pop(&(key as _)) };
41+
pub(crate) fn remove_from_cache(key: &Rc<dyn IMemo>) -> bool {
42+
unsafe { CACHE.pop(&Rc::as_ptr(key)) }.is_some()
4543
}

cache/src/call_stack.rs

Lines changed: 0 additions & 19 deletions
This file was deleted.

cache/src/lib.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub(crate) mod cache;
2-
pub(crate) mod call_stack;
32
pub(crate) mod effect_stack;
3+
pub(crate) mod memo_stack;
4+
pub(crate) mod observable;
45

56
pub mod effect;
67
pub mod macros;
@@ -9,13 +10,11 @@ pub mod signal;
910

1011
pub(crate) use cache::{remove_from_cache, store_in_cache, touch};
1112
pub use effect::Effect;
13+
pub(crate) use memo::IMemo;
1214
pub use memo::Memo;
15+
pub(crate) use observable::IObservable;
1316
pub use signal::Signal;
1417

15-
pub(crate) trait Observable {
16-
fn invalidate(&'static self);
17-
}
18-
1918
pub use once_cell::unsync::Lazy;
2019

2120
#[cfg(feature = "macros")]

cache/src/memo.rs

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use std::cell::RefCell;
1+
use std::{
2+
cell::RefCell,
3+
rc::{Rc, Weak},
4+
};
25

3-
use crate::{Observable, call_stack, remove_from_cache, store_in_cache, touch};
6+
use crate::{IObservable, memo_stack, store_in_cache, touch};
47

58
/// A memoized reactive computation that caches its result and tracks dependencies.
69
///
@@ -17,31 +20,36 @@ use crate::{Observable, call_stack, remove_from_cache, store_in_cache, touch};
1720
/// - `T`: The result type of the computation. Must implement `Clone`.
1821
pub struct Memo<T> {
1922
f: Box<dyn Fn() -> T>,
20-
dependents: RefCell<Vec<&'static dyn Observable>>,
21-
}
22-
23-
impl<T> Observable for Memo<T> {
24-
fn invalidate(&'static self) {
25-
remove_from_cache(self);
26-
self.dependents.borrow().iter().for_each(|d| d.invalidate());
27-
}
23+
dependents: RefCell<Vec<Weak<dyn IMemo>>>,
24+
/// A self-referential weak pointer, set during construction with `Rc::new_cyclic`.
25+
/// Used to upgrade to `Rc<Memo<T>>` and then coerce into `Rc<dyn IMemo>` when needed.
26+
weak: Weak<Memo<T>>,
2827
}
2928

3029
impl<T> Memo<T> {
3130
/// Creates a new `Memo` wrapping the provided closure.
3231
///
32+
/// # Requirements
33+
/// - `T` must be `'static`, because the value is stored in global cache.
34+
/// - The closure must be `'static` as well.
35+
///
3336
/// # Examples
3437
///
3538
/// ```
3639
/// use reactive_cache::Memo;
3740
///
3841
/// let memo = Memo::new(|| 10);
42+
/// assert_eq!(memo.get(), 10);
3943
/// ```
40-
pub fn new(f: impl Fn() -> T + 'static) -> Self {
41-
Memo {
44+
pub fn new(f: impl Fn() -> T + 'static) -> Rc<Self>
45+
where
46+
T: 'static,
47+
{
48+
Rc::new_cyclic(|weak| Memo {
4249
f: Box::new(f),
4350
dependents: vec![].into(),
44-
}
51+
weak: weak.clone(),
52+
})
4553
}
4654

4755
/// Returns the memoized value, recomputing it only if necessary.
@@ -51,37 +59,45 @@ impl<T> Memo<T> {
5159
/// # Examples
5260
///
5361
/// ```
54-
/// use once_cell::unsync::Lazy;
5562
/// use reactive_cache::Memo;
5663
///
57-
/// static mut MEMO: Lazy<Memo<i32>> = Lazy::new(|| Memo::new(|| 5));
58-
/// assert_eq!(unsafe { (*MEMO).get() }, 5);
64+
/// let memo = Memo::new(|| 5);
65+
/// assert_eq!(memo.get(), 5);
5966
/// ```
60-
pub fn get(&'static self) -> T
67+
pub fn get(&self) -> T
6168
where
62-
T: Clone,
69+
T: Clone + 'static,
6370
{
64-
if let Some(last) = call_stack::last()
65-
&& !self
66-
.dependents
67-
.borrow()
68-
.iter()
69-
.any(|d| std::ptr::eq(*d, *last))
70-
{
71-
self.dependents.borrow_mut().push(*last);
72-
}
71+
self.dependency_collection();
7372

74-
call_stack::push(self);
73+
memo_stack::push(self.weak.clone());
7574

76-
let rc = if let Some(rc) = touch(self) {
77-
rc
75+
let rc = if let Some(this) = self.weak.upgrade() {
76+
let key: Rc<dyn IMemo> = this.clone();
77+
if let Some(rc) = touch(&key) {
78+
rc
79+
} else {
80+
let result: T = (self.f)();
81+
store_in_cache(&key, result)
82+
}
7883
} else {
79-
let result: T = (self.f)();
80-
store_in_cache(self, result)
84+
unreachable!()
8185
};
8286

83-
call_stack::pop();
87+
memo_stack::pop();
8488

8589
(*rc).clone()
8690
}
8791
}
92+
93+
impl<T> IObservable for Memo<T> {
94+
fn dependents(&self) -> &RefCell<Vec<Weak<dyn IMemo>>> {
95+
&self.dependents
96+
}
97+
}
98+
99+
/// Internal marker trait for all memoized computations.
100+
/// Used for type erasure when storing heterogeneous `Memo<T>` in caches.
101+
pub(crate) trait IMemo: IObservable {}
102+
103+
impl<T> IMemo for Memo<T> {}

cache/src/memo_stack.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#![allow(static_mut_refs)]
2+
3+
use std::rc::Weak;
4+
5+
use once_cell::sync::Lazy;
6+
7+
use crate::IMemo;
8+
9+
static mut MEMO_STACK: Lazy<Vec<Weak<dyn IMemo>>> = Lazy::new(Vec::new);
10+
11+
pub(crate) fn push(op: Weak<dyn IMemo>) {
12+
unsafe { MEMO_STACK.push(op) }
13+
}
14+
15+
pub(crate) fn last() -> Option<&'static Weak<dyn IMemo>> {
16+
unsafe { MEMO_STACK.last() }
17+
}
18+
19+
pub(crate) fn pop() -> Option<Weak<dyn IMemo>> {
20+
unsafe { MEMO_STACK.pop() }
21+
}

cache/src/observable.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use std::{cell::RefCell, rc::Weak};
2+
3+
use crate::{IMemo, memo_stack, remove_from_cache};
4+
5+
pub(crate) trait IObservable {
6+
fn dependents(&self) -> &RefCell<Vec<Weak<dyn IMemo>>>;
7+
8+
/// Invalidates all dependent observables.
9+
fn invalidate(&self) {
10+
self.dependents().borrow_mut().retain(|d| {
11+
if let Some(d) = d.upgrade() {
12+
remove_from_cache(&d);
13+
d.invalidate();
14+
true
15+
} else {
16+
false
17+
}
18+
});
19+
}
20+
21+
/// Track observables in the call stack
22+
fn dependency_collection(&self) {
23+
if let Some(last) = memo_stack::last()
24+
&& !self
25+
.dependents()
26+
.borrow()
27+
.iter()
28+
.any(|d| Weak::ptr_eq(d, last))
29+
{
30+
self.dependents().borrow_mut().push(last.clone());
31+
}
32+
}
33+
}

cache/src/signal.rs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
rc::Weak,
44
};
55

6-
use crate::{Effect, Observable, call_stack, effect_stack::EffectStackEntry};
6+
use crate::{Effect, IMemo, IObservable, effect_stack::EffectStackEntry};
77

88
/// A reactive signal that holds a value, tracks dependencies, and triggers effects.
99
///
@@ -22,17 +22,15 @@ use crate::{Effect, Observable, call_stack, effect_stack::EffectStackEntry};
2222
/// - `T`: The type of the value stored in the signal. Must implement `Eq + Default`.
2323
pub struct Signal<T> {
2424
value: RefCell<T>,
25-
dependents: RefCell<Vec<&'static dyn Observable>>,
25+
dependents: RefCell<Vec<Weak<dyn IMemo>>>,
2626
effects: RefCell<Vec<Weak<Effect>>>,
2727
}
2828

2929
impl<T> Signal<T> {
30-
/// Invalidates all dependent observables.
31-
fn invalidate(&self) {
32-
self.dependents.borrow().iter().for_each(|d| d.invalidate());
33-
}
34-
35-
/// Runs all dependent effects.
30+
/// Re-runs all dependent effects that are still alive.
31+
///
32+
/// This is triggered after the signal's value has changed.
33+
/// Dead effects (already dropped) are cleaned up automatically.
3634
fn flush_effects(&self) {
3735
// When triggering an Effect, dependencies are not collected for that Effect.
3836
self.effects.borrow_mut().retain(|w| {
@@ -45,11 +43,15 @@ impl<T> Signal<T> {
4543
});
4644
}
4745

46+
/// Called after the value is updated.
47+
/// Triggers all dependent effects.
4848
#[allow(non_snake_case)]
4949
fn OnPropertyChanged(&self) {
5050
self.flush_effects()
5151
}
5252

53+
/// Called before the value is updated.
54+
/// Invalidates all memoized computations depending on this signal.
5355
#[allow(non_snake_case)]
5456
fn OnPropertyChanging(&self) {
5557
self.invalidate()
@@ -81,7 +83,8 @@ impl<T> Signal<T> {
8183
}
8284
}
8385

84-
/// Gets a reference to the current value, tracking dependencies and effects if inside a reactive context.
86+
/// Gets a reference to the current value, tracking dependencies
87+
/// and effects if inside a reactive context.
8588
///
8689
/// # Examples
8790
///
@@ -92,16 +95,7 @@ impl<T> Signal<T> {
9295
/// assert_eq!(*signal.get(), 42);
9396
/// ```
9497
pub fn get(&self) -> Ref<'_, T> {
95-
// Track observables in the call stack
96-
if let Some(last) = call_stack::last()
97-
&& !self
98-
.dependents
99-
.borrow()
100-
.iter()
101-
.any(|d| std::ptr::eq(*d, *last))
102-
{
103-
self.dependents.borrow_mut().push(*last);
104-
}
98+
self.dependency_collection();
10599

106100
// Track effects in the call stack
107101
if let Some(EffectStackEntry {
@@ -119,7 +113,8 @@ impl<T> Signal<T> {
119113

120114
/// Sets the value of the signal.
121115
///
122-
/// Returns `true` if the value changed and dependent effects were triggered.
116+
/// Returns `true` if the value changed, all dependent memos are
117+
/// invalidated and dependent effects were triggered.
123118
///
124119
/// # Examples
125120
///
@@ -150,3 +145,9 @@ impl<T> Signal<T> {
150145
true
151146
}
152147
}
148+
149+
impl<T> IObservable for Signal<T> {
150+
fn dependents(&self) -> &RefCell<Vec<Weak<dyn IMemo>>> {
151+
&self.dependents
152+
}
153+
}

macros/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ pub fn memo(_attr: TokenStream, item: TokenStream) -> TokenStream {
241241
}
242242

243243
let ident = format_ident!("{}", ident.to_string().to_uppercase());
244-
let ty = quote! { reactive_cache::Lazy<reactive_cache::Memo<#output_ty>> };
244+
let ty = quote! { reactive_cache::Lazy<std::rc::Rc<reactive_cache::Memo<#output_ty>>> };
245245
let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Memo::new(|| #block)) };
246246

247247
let expanded = quote! {

0 commit comments

Comments
 (0)