Merge origin/main into macos
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,12 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
tracing = { version = "0.1", default-features = false }
|
||||
hashbrown = "0.17"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "std"] }
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"std",
|
||||
] }
|
||||
|
||||
152
lib/reactivity/README.md
Normal file
152
lib/reactivity/README.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# RUIN Reactivity - Fine-Grained Reactor
|
||||
|
||||
Automatic thread-stack-based reactivity for RUIN-apps.
|
||||
|
||||
RUIN Reactivity provides an implementation of fine-grained reactivity primitives
|
||||
for dependency tracking and incremental computation. Those primitives are:
|
||||
|
||||
- `Cell<T>`: a tracked-state value cell, primitively observable (sometimes
|
||||
called a "signal" in other reactor implementations).
|
||||
- `effect`: a primitive observer that runs once, observes its dependencies, and
|
||||
runs again whenever its dependencies change.
|
||||
- `Thunk<T>`: a tracked-state recomputable value defined by a closure.
|
||||
Invalidated if any of its dependencies change. A `Thunk` is both an observer
|
||||
and an observable.
|
||||
- `Event<T>`: a push-style source of events of type `T`. Supports subscription
|
||||
and cancellation of interest in events.
|
||||
|
||||
RUIN reactivity requires the RUIN runtime to function, and cannot be used on
|
||||
threads not managed by the RUIN runtime.
|
||||
|
||||
RUIN reactivity does not function across thread boundaries. It tracks
|
||||
dependencies between entities on the same thread only.
|
||||
|
||||
## Examples
|
||||
|
||||
### Observe a cell using an effect
|
||||
|
||||
```rs
|
||||
use std::time::Duration;
|
||||
|
||||
use ruin_reactivity::{cell, effect};
|
||||
use ruin_runtime::{main, set_timeout};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
// Creates an observable value. Calling `.get` from within an observer will create a dependency, and calling `.set`
|
||||
// will trigger updates to any dependent observers.
|
||||
let v = cell(5);
|
||||
|
||||
// Creates an observer that prints the value of `v` whenever it changes.
|
||||
// Calling `.leak()` on the effect handle allows it to run for the lifetime of the program without automatically
|
||||
// disposing when dropped.
|
||||
effect({
|
||||
let v = v.clone();
|
||||
move || {
|
||||
println!("v is: {}", v.get());
|
||||
}
|
||||
})
|
||||
.leak();
|
||||
|
||||
// Queue a future to wait 5 seconds and then update `v`. This will trigger
|
||||
// the effect to run again and print the new value.
|
||||
set_timeout(Duration::from_secs(5), {
|
||||
let v = v.clone();
|
||||
move || {
|
||||
v.set(v.get() + 20);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Observe a thunk using an effect
|
||||
|
||||
```rs
|
||||
use std::time::Duration;
|
||||
|
||||
use ruin_reactivity::{cell, effect, thunk};
|
||||
use ruin_runtime::{clear_interval, main, set_interval, set_timeout};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
// Two primitive observable values.
|
||||
let x = cell(5);
|
||||
let y = cell(10);
|
||||
|
||||
// A derived observable value that depends on `x` and `y`. The closure will only run when `x` or `y` change, and the
|
||||
// result will be cached until then.
|
||||
let z = thunk({
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move || {
|
||||
println!("calculating z...");
|
||||
x.get() + y.get()
|
||||
}
|
||||
});
|
||||
|
||||
// The effect observes `z`, so it will run whenever `z` changes. Because `z` depends on `x` and `y`, the effect will
|
||||
// run whenever `x` or `y` change.
|
||||
effect({
|
||||
let z = z.clone();
|
||||
move || {
|
||||
println!("z is: {}", z.get());
|
||||
}
|
||||
})
|
||||
.leak();
|
||||
|
||||
// Update `x` and `y` every second. This will trigger the effect to run and print the new value of `z`.
|
||||
let interval = set_interval(Duration::from_secs(1), {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move || {
|
||||
x.update(|value| *value += 1);
|
||||
y.update(|value| *value += 2);
|
||||
}
|
||||
});
|
||||
|
||||
// After 10 seconds, clear the interval to stop updating `x` and `y`. Once the interval is cleared, the queue will
|
||||
// empty and the program will exit since there are no more pending tasks.
|
||||
set_timeout(Duration::from_secs(10), move || {
|
||||
println!("clearing interval...");
|
||||
clear_interval(&interval);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Use an event to handle intra-thread messaging
|
||||
|
||||
```rs
|
||||
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||
|
||||
use ruin_reactivity::event;
|
||||
use ruin_runtime::{main, queue_future, set_interval, time::sleep};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
let my_event = event::<String>();
|
||||
|
||||
my_event.subscribe(|message| {
|
||||
println!("got event with message: {message}");
|
||||
});
|
||||
|
||||
// Emit an event every 250ms with an incrementing count.
|
||||
let interval = set_interval(Duration::from_millis(250), {
|
||||
let counter = Rc::new(Cell::new(0));
|
||||
move || {
|
||||
let count = counter.get();
|
||||
my_event.emit(format!("the count is {}", count));
|
||||
counter.set(count + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// After 5 seconds, clear the interval to stop emitting events.
|
||||
queue_future(async move {
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
interval.clear();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT License](../../LICENSE.txt).
|
||||
31
lib/reactivity/examples/readme_md_01.rs
Normal file
31
lib/reactivity/examples/readme_md_01.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ruin_reactivity::{cell, effect};
|
||||
use ruin_runtime::{main, set_timeout};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
// Creates an observable value. Calling `.get` from within an observer will create a dependency, and calling `.set`
|
||||
// will trigger updates to any dependent observers.
|
||||
let v = cell(5);
|
||||
|
||||
// Creates an observer that prints the value of `v` whenever it changes.
|
||||
// Calling `.leak()` on the effect handle allows it to run for the lifetime of the program without automatically
|
||||
// disposing when dropped.
|
||||
effect({
|
||||
let v = v.clone();
|
||||
move || {
|
||||
println!("v is: {}", v.get());
|
||||
}
|
||||
})
|
||||
.leak();
|
||||
|
||||
// Queue a future to wait 5 seconds and then update `v`. This will trigger
|
||||
// the effect to run again and print the new value.
|
||||
set_timeout(Duration::from_secs(5), {
|
||||
let v = v.clone();
|
||||
move || {
|
||||
v.set(v.get() + 20);
|
||||
}
|
||||
});
|
||||
}
|
||||
49
lib/reactivity/examples/readme_md_02.rs
Normal file
49
lib/reactivity/examples/readme_md_02.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ruin_reactivity::{cell, effect, thunk};
|
||||
use ruin_runtime::{clear_interval, main, set_interval, set_timeout};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
// Two primitive observable values.
|
||||
let x = cell(5);
|
||||
let y = cell(10);
|
||||
|
||||
// A derived observable value that depends on `x` and `y`. The closure will only run when `x` or `y` change, and the
|
||||
// result will be cached until then.
|
||||
let z = thunk({
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move || {
|
||||
println!("calculating z...");
|
||||
x.get() + y.get()
|
||||
}
|
||||
});
|
||||
|
||||
// The effect observes `z`, so it will run whenever `z` changes. Because `z` depends on `x` and `y`, the effect will
|
||||
// run whenever `x` or `y` change.
|
||||
effect({
|
||||
let z = z.clone();
|
||||
move || {
|
||||
println!("z is: {}", z.get());
|
||||
}
|
||||
})
|
||||
.leak();
|
||||
|
||||
// Update `x` and `y` every second. This will trigger the effect to run and print the new value of `z`.
|
||||
let interval = set_interval(Duration::from_secs(1), {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move || {
|
||||
x.update(|value| *value += 1);
|
||||
y.update(|value| *value += 2);
|
||||
}
|
||||
});
|
||||
|
||||
// After 10 seconds, clear the interval to stop updating `x` and `y`. Once the interval is cleared, the queue will
|
||||
// empty and the program will exit since there are no more pending tasks.
|
||||
set_timeout(Duration::from_secs(10), move || {
|
||||
println!("clearing interval...");
|
||||
clear_interval(&interval);
|
||||
});
|
||||
}
|
||||
29
lib/reactivity/examples/readme_md_03.rs
Normal file
29
lib/reactivity/examples/readme_md_03.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||
|
||||
use ruin_reactivity::event;
|
||||
use ruin_runtime::{main, queue_future, set_interval, time::sleep};
|
||||
|
||||
#[main]
|
||||
fn main() {
|
||||
let my_event = event::<String>();
|
||||
|
||||
my_event.subscribe(|message| {
|
||||
println!("got event with message: {message}");
|
||||
});
|
||||
|
||||
// Emit an event every 250ms with an incrementing count.
|
||||
let interval = set_interval(Duration::from_millis(250), {
|
||||
let counter = Rc::new(Cell::new(0));
|
||||
move || {
|
||||
let count = counter.get();
|
||||
my_event.emit(format!("the count is {}", count));
|
||||
counter.set(count + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// After 5 seconds, clear the interval to stop emitting events.
|
||||
queue_future(async move {
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
interval.clear();
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::RefCell;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
@@ -114,7 +114,7 @@ impl<T: PartialEq + 'static> Cell<T> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let previous = std::mem::replace(&mut *current, value);
|
||||
let previous = core::mem::replace(&mut *current, value);
|
||||
drop(current);
|
||||
tracing::debug!(
|
||||
target: trace_targets::CELL,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::{Rc, Weak};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::{Rc, Weak};
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use crate::reactor::ObserverHook;
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
/// Creates an effect in the current thread's default reactor.
|
||||
///
|
||||
/// The effect is scheduled immediately and then re-scheduled whenever one of its dependencies
|
||||
/// changes.
|
||||
pub fn effect(f: impl Fn() + 'static) -> EffectHandle {
|
||||
current().effect(f)
|
||||
}
|
||||
@@ -16,6 +20,7 @@ pub fn effect_in(reactor: &Reactor, f: impl Fn() + 'static) -> EffectHandle {
|
||||
|
||||
/// Disposable handle for a reactive effect.
|
||||
#[derive(Clone)]
|
||||
#[must_use = "effects are automatically disposed when dropped, so you must use the handle or explicitly leak it"]
|
||||
pub struct EffectHandle {
|
||||
inner: Rc<EffectInner>,
|
||||
}
|
||||
@@ -55,6 +60,14 @@ impl EffectHandle {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Consumes the effect and leaks it, allowing it to run for the remainder of the program without automatically
|
||||
/// disposing when dropped. Use this for effects that you want to run for the lifetime of the program. You CANNOT
|
||||
/// recover an EffectHandle after calling this method, so be sure to call it on an EffectHandle that you don't need
|
||||
/// to later dispose of.
|
||||
pub fn leak(self) {
|
||||
core::mem::forget(self);
|
||||
}
|
||||
|
||||
/// Disposes the effect immediately.
|
||||
pub fn dispose(&self) {
|
||||
self.inner.dispose();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
@@ -17,11 +20,13 @@ pub fn event_in<T: 'static>(reactor: &Reactor) -> Event<T> {
|
||||
}
|
||||
|
||||
/// Creates a reactive draining subscription for `event` in the current reactor.
|
||||
#[must_use = "subscriptions created with `on` must be used to stay active"]
|
||||
pub fn on<T: Clone + 'static>(event: &Event<T>, handler: impl Fn(&T) + 'static) -> Subscription {
|
||||
current().on(event, handler)
|
||||
}
|
||||
|
||||
/// Creates a reactive draining subscription for `event` associated with `reactor`.
|
||||
#[must_use = "subscriptions created with `on_in` must be used to stay active"]
|
||||
pub fn on_in<T: Clone + 'static>(
|
||||
reactor: &Reactor,
|
||||
event: &Event<T>,
|
||||
@@ -49,6 +54,7 @@ impl Reactor {
|
||||
}
|
||||
|
||||
/// Creates a reactive draining subscription for `event`.
|
||||
#[must_use = "subscriptions created with `on` must be used to stay active"]
|
||||
pub fn on<T: Clone + 'static>(
|
||||
&self,
|
||||
event: &Event<T>,
|
||||
@@ -103,7 +109,7 @@ impl<T: 'static> Event<T> {
|
||||
reactor,
|
||||
id,
|
||||
next_subscriber: Cell::new(1),
|
||||
subscribers: RefCell::new(BTreeMap::new()),
|
||||
subscribers: RefCell::new(Default::default()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -204,7 +210,7 @@ struct EventInner<T> {
|
||||
reactor: Reactor,
|
||||
id: NodeId,
|
||||
next_subscriber: Cell<usize>,
|
||||
subscribers: RefCell<BTreeMap<usize, Rc<SubscriberFn<T>>>>,
|
||||
subscribers: RefCell<HashMap<usize, Rc<SubscriberFn<T>>>>,
|
||||
}
|
||||
|
||||
struct SubscriptionInner {
|
||||
@@ -227,11 +233,12 @@ impl SubscriptionInner {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SubscriptionInner {
|
||||
fn drop(&mut self) {
|
||||
self.unsubscribe();
|
||||
}
|
||||
}
|
||||
// TODO: it's actually kind of hard to use events if subcscriptions have to be manually managed.
|
||||
// impl Drop for SubscriptionInner {
|
||||
// fn drop(&mut self) {
|
||||
// self.unsubscribe();
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! single-threaded and designed to live on a runtime-managed thread, while async work feeds it
|
||||
//! from the edges by updating state or emitting events.
|
||||
|
||||
#![feature(thread_local)]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub(crate) mod trace_targets {
|
||||
pub const GRAPH: &str = "ruin_reactivity::graph";
|
||||
pub const CELL: &str = "ruin_reactivity::cell";
|
||||
@@ -18,6 +23,7 @@ mod effect;
|
||||
mod event;
|
||||
mod id;
|
||||
mod reactor;
|
||||
mod source;
|
||||
mod thunk;
|
||||
|
||||
pub use cell::{Cell, cell, cell_in};
|
||||
@@ -25,4 +31,5 @@ pub use effect::{EffectHandle, effect, effect_in};
|
||||
pub use event::{Event, Subscription, event, event_in, on, on_in};
|
||||
pub use id::NodeId;
|
||||
pub use reactor::{ReactCycleError, Reactor, current};
|
||||
pub use source::{Source, source, source_in};
|
||||
pub use thunk::{Memo, Thunk, memo, memo_by, memo_by_in, memo_in, thunk, thunk_in};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::panic::panic_any;
|
||||
use std::rc::{Rc, Weak};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::VecDeque;
|
||||
use alloc::rc::{Rc, Weak};
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::error::Error;
|
||||
use core::fmt;
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
|
||||
use ruin_runtime::queue_microtask;
|
||||
|
||||
@@ -11,9 +14,8 @@ use crate::{NodeId, trace_targets};
|
||||
|
||||
type Job = Box<dyn FnOnce() + 'static>;
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_REACTOR: RefCell<Weak<ReactorInner>> = const { RefCell::new(Weak::new()) };
|
||||
}
|
||||
#[thread_local]
|
||||
static CURRENT_REACTOR: RefCell<Weak<ReactorInner>> = const { RefCell::new(Weak::new()) };
|
||||
|
||||
/// Returns the current thread's default reactor.
|
||||
///
|
||||
@@ -28,7 +30,7 @@ pub(crate) trait ObserverHook {
|
||||
fn notify(&self);
|
||||
}
|
||||
|
||||
/// Panic payload emitted when the reactor detects a dependency cycle.
|
||||
/// Error type for cycles detected in the reactive graph. Contains the path of nodes that form the cycle.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ReactCycleError {
|
||||
cycle: Vec<NodeId>,
|
||||
@@ -85,26 +87,24 @@ impl Reactor {
|
||||
|
||||
/// Returns the current thread's default reactor.
|
||||
pub fn current() -> Self {
|
||||
CURRENT_REACTOR.with(|slot| {
|
||||
if let Some(inner) = slot.borrow().upgrade() {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_reuse",
|
||||
"reusing current thread default reactor"
|
||||
);
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
let reactor = Self::new();
|
||||
*slot.borrow_mut() = Rc::downgrade(&reactor.inner);
|
||||
tracing::debug!(
|
||||
if let Some(inner) = CURRENT_REACTOR.borrow().upgrade() {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_install",
|
||||
"installed current thread default reactor"
|
||||
event = "current_reactor_reuse",
|
||||
"reusing current thread default reactor"
|
||||
);
|
||||
reactor
|
||||
})
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
let reactor = Self::new();
|
||||
*CURRENT_REACTOR.borrow_mut() = Rc::downgrade(&reactor.inner);
|
||||
tracing::debug!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "current_reactor_install",
|
||||
"installed current thread default reactor"
|
||||
);
|
||||
reactor
|
||||
}
|
||||
|
||||
/// Runs `f` in the dependency-tracking scope of `observer`.
|
||||
@@ -143,13 +143,9 @@ impl Reactor {
|
||||
f()
|
||||
}
|
||||
|
||||
/// Records that the currently executing observer depends on `observable`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics with [`ReactCycleError`] if `observable` is already being computed in the current
|
||||
/// dependency stack.
|
||||
pub fn observe(&self, observable: NodeId) {
|
||||
/// Attempts to record a dependency on `observable` for the currently running observer, returning an
|
||||
/// error if doing so would create a dependency cycle.
|
||||
pub fn try_observe(&self, observable: NodeId) -> Result<(), ReactCycleError> {
|
||||
if self
|
||||
.inner
|
||||
.active_computations
|
||||
@@ -170,12 +166,12 @@ impl Reactor {
|
||||
cycle_len = cycle.len(),
|
||||
"reactive cycle detected"
|
||||
);
|
||||
panic_any(ReactCycleError::new(cycle));
|
||||
return Err(ReactCycleError::new(cycle));
|
||||
}
|
||||
|
||||
let current = self.inner.stack.borrow().last().copied();
|
||||
let Some(observer) = current else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -199,6 +195,18 @@ impl Reactor {
|
||||
.entry(observable)
|
||||
.or_default()
|
||||
.insert(observer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records a dependency on `observable` for the currently running observer.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if recording the dependency would create a cycle in the reactive graph.
|
||||
pub fn observe(&self, observable: NodeId) {
|
||||
if let Err(e) = self.try_observe(observable) {
|
||||
panic!("reactive cycle detected: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies dependents of `observable`.
|
||||
@@ -352,11 +360,11 @@ impl fmt::Debug for Reactor {
|
||||
|
||||
struct ReactorInner {
|
||||
next_node: Cell<u64>,
|
||||
dependencies: RefCell<BTreeMap<NodeId, BTreeSet<NodeId>>>,
|
||||
dependents: RefCell<BTreeMap<NodeId, BTreeSet<NodeId>>>,
|
||||
observers: RefCell<BTreeMap<NodeId, Weak<dyn ObserverHook>>>,
|
||||
dependencies: RefCell<HashMap<NodeId, HashSet<NodeId>>>,
|
||||
dependents: RefCell<HashMap<NodeId, HashSet<NodeId>>>,
|
||||
observers: RefCell<HashMap<NodeId, Weak<dyn ObserverHook>>>,
|
||||
stack: RefCell<Vec<NodeId>>,
|
||||
active_computations: RefCell<BTreeSet<NodeId>>,
|
||||
active_computations: RefCell<HashSet<NodeId>>,
|
||||
pending_jobs: RefCell<VecDeque<Job>>,
|
||||
flush_scheduled: Cell<bool>,
|
||||
}
|
||||
@@ -365,11 +373,11 @@ impl ReactorInner {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
next_node: Cell::new(1),
|
||||
dependencies: RefCell::new(BTreeMap::new()),
|
||||
dependents: RefCell::new(BTreeMap::new()),
|
||||
observers: RefCell::new(BTreeMap::new()),
|
||||
dependencies: RefCell::new(HashMap::new()),
|
||||
dependents: RefCell::new(HashMap::new()),
|
||||
observers: RefCell::new(HashMap::new()),
|
||||
stack: RefCell::new(Vec::new()),
|
||||
active_computations: RefCell::new(BTreeSet::new()),
|
||||
active_computations: RefCell::new(HashSet::new()),
|
||||
pending_jobs: RefCell::new(VecDeque::new()),
|
||||
flush_scheduled: Cell::new(false),
|
||||
}
|
||||
@@ -429,7 +437,7 @@ mod tests {
|
||||
|
||||
use ruin_runtime::{queue_task, run};
|
||||
|
||||
use super::{ReactCycleError, Reactor, current};
|
||||
use super::{Reactor, current};
|
||||
|
||||
#[test]
|
||||
fn current_reactor_is_thread_local_singleton() {
|
||||
@@ -444,7 +452,11 @@ mod tests {
|
||||
let observer = reactor.allocate_node();
|
||||
let observable = reactor.allocate_node();
|
||||
|
||||
reactor.run_in_context(observer, || reactor.observe(observable));
|
||||
reactor.run_in_context(observer, || {
|
||||
reactor
|
||||
.try_observe(observable)
|
||||
.expect("should not detect cycle")
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
reactor.inner.dependencies.borrow().get(&observer),
|
||||
@@ -457,7 +469,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_detection_panics_with_structured_error() {
|
||||
fn cycle_detection_panics_with_expected() {
|
||||
let reactor = Reactor::new();
|
||||
let a = reactor.allocate_node();
|
||||
let b = reactor.allocate_node();
|
||||
@@ -465,15 +477,26 @@ mod tests {
|
||||
let panic = catch_unwind(AssertUnwindSafe(|| {
|
||||
reactor.run_in_context(a, || {
|
||||
reactor.observe(b);
|
||||
reactor.run_in_context(b, || reactor.observe(a));
|
||||
reactor.run_in_context(b, || {
|
||||
reactor.observe(a);
|
||||
});
|
||||
});
|
||||
}))
|
||||
.expect_err("cycle should panic");
|
||||
|
||||
let error = panic
|
||||
.downcast::<ReactCycleError>()
|
||||
.expect("panic payload should be ReactCycleError");
|
||||
assert_eq!(error.cycle(), &[a, b, a]);
|
||||
let Some(cycle_error) = panic.downcast_ref::<String>() else {
|
||||
panic!("panic should be a string");
|
||||
};
|
||||
|
||||
assert!(
|
||||
cycle_error.contains("reactive cycle detected"),
|
||||
"panic should indicate cycle detected"
|
||||
);
|
||||
|
||||
assert!(
|
||||
cycle_error.contains("1 -> 2 -> 1"),
|
||||
"panic should include cycle path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
70
lib/reactivity/src/source.rs
Normal file
70
lib/reactivity/src/source.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use alloc::rc::Rc;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
|
||||
/// Creates a low-level reactive source node in the current reactor.
|
||||
pub fn source() -> Source {
|
||||
current().source()
|
||||
}
|
||||
|
||||
/// Creates a low-level reactive source node associated with `reactor`.
|
||||
pub fn source_in(reactor: &Reactor) -> Source {
|
||||
reactor.source()
|
||||
}
|
||||
|
||||
/// Low-level observable source node.
|
||||
///
|
||||
/// `Source` is useful for advanced data structures that want precise control over when reads
|
||||
/// observe and writes trigger invalidation without storing their state in a [`crate::Cell`].
|
||||
#[derive(Clone)]
|
||||
pub struct Source {
|
||||
inner: Rc<SourceInner>,
|
||||
}
|
||||
|
||||
impl Reactor {
|
||||
/// Creates a low-level source node associated with this reactor.
|
||||
pub fn source(&self) -> Source {
|
||||
Source::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn new(reactor: Reactor) -> Self {
|
||||
let id = reactor.allocate_node();
|
||||
tracing::debug!(
|
||||
target: trace_targets::GRAPH,
|
||||
event = "create_source",
|
||||
node_id = id.0,
|
||||
"created low-level reactive source"
|
||||
);
|
||||
Self {
|
||||
inner: Rc::new(SourceInner { reactor, id }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a dependency on this source for the currently running observer.
|
||||
pub fn observe(&self) {
|
||||
self.inner.reactor.observe(self.inner.id);
|
||||
}
|
||||
|
||||
/// Triggers this source's dependents.
|
||||
pub fn trigger(&self) {
|
||||
self.inner.reactor.trigger(self.inner.id);
|
||||
}
|
||||
|
||||
/// Returns the source node id.
|
||||
pub fn id(&self) -> NodeId {
|
||||
self.inner.id
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceInner {
|
||||
reactor: Reactor,
|
||||
id: NodeId,
|
||||
}
|
||||
|
||||
impl Drop for SourceInner {
|
||||
fn drop(&mut self) {
|
||||
self.reactor.dispose(self.id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
use crate::reactor::ObserverHook;
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
@@ -428,9 +429,13 @@ mod tests {
|
||||
.expect_err("mutually recursive thunks should panic");
|
||||
|
||||
let error = panic
|
||||
.downcast::<crate::ReactCycleError>()
|
||||
.expect("panic payload should be ReactCycleError");
|
||||
assert_eq!(error.cycle().len(), 3);
|
||||
.downcast_ref::<String>()
|
||||
.expect("panic should be a string");
|
||||
|
||||
assert!(
|
||||
error.contains("reactive cycle detected"),
|
||||
"panic should indicate cycle detected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user