153 lines
4.5 KiB
Markdown
153 lines
4.5 KiB
Markdown
# 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).
|