Refactor ruin_app, add some README files, minor usability in reactivity
This commit is contained in:
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();
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,9 @@ 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 +19,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 +59,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) {
|
||||
std::mem::forget(self);
|
||||
}
|
||||
|
||||
/// Disposes the effect immediately.
|
||||
pub fn dispose(&self) {
|
||||
self.inner.dispose();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{NodeId, Reactor, current, trace_targets};
|
||||
@@ -17,11 +17,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 +51,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 +106,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 +207,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 +230,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 {
|
||||
|
||||
Reference in New Issue
Block a user