1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
//! [`Runtime`]s are the primary integration point between moxie and
//! embedding environments.
mod context;
mod runloop;
mod var;
use dyn_cache::local::SharedLocalCache;
use futures::{
future::LocalFutureObj,
task::{noop_waker, LocalSpawn, SpawnError},
};
use illicit::AsContext;
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
rc::Rc,
task::Waker,
};
pub(crate) use context::Context;
pub use runloop::RunLoop;
pub(crate) use var::Var;
/// Revisions measure moxie's notion of time passing. Each `Runtime` increments
/// its Revision on every iteration. `crate::Commit`s to state variables are
/// annotated with the Revision during which they were made.
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Revision(pub u64);
impl Revision {
/// Returns the current revision. Will return `Revision(0)` if called
/// outside of a Runtime's execution.
pub fn current() -> Self {
if let Ok(r) = illicit::get::<Context>() {
r.revision()
} else {
Revision::default()
}
}
}
impl std::fmt::Debug for Revision {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_fmt(format_args!("r{}", self.0))
}
}
/// A [`Runtime`] is the primary integration point between moxie and an
/// embedder. Each independent instance is responsible for an event loop and
/// tracks time using a [`Revision`] which it increments on each iteration of
/// the loop. Owns the cache and state for a given context.
///
/// # Key considerations
///
/// ## Event Loops
///
/// moxie assumes that it will be responsible for interfacing with or managing
/// an event loop with the basic structure:
///
/// 1. enter loop
/// 2. present interface
/// 3. wait for changes
/// 4. goto (2)
///
/// Step (2) is implemented in [`Runtime::run_once`], and the user code run by
/// that method is expected to register the runtime for wakeups on future
/// events. Step (3) is implemented by the embedder, waiting until the runtime's
/// change notification waker is invoked.
///
/// ## Change notifications
///
/// Each runtime should be provided with a [`std::task::Waker`] that will notify
/// the embedding environment to run the loop again. This is done by calling
/// [`Runtime::set_state_change_waker`].
///
/// For scenarios without an obvious "main thread" this can be done for you by
/// binding a root function to a [`RunLoop`] which implements
/// [`std::future::Future`] and can be spawned as a task onto an executor. For
/// more nuanced scenarios it can be necessary to write your own waker to ensure
/// scheduling compatible with the embedding environment. By default a no-op
/// waker is provided.
///
/// The most common way of notifying a runtime of a change is to update a
/// state variable.
///
/// ## Caching
///
/// When a runtime is repeatedly invoking the same code for every [`Revision`]
/// there's likely to be a lot of repetitive work. This might be something
/// complex like a slow computation over the set of visible items, or it might
/// be something simple like using the same DOM node for a given button on every
/// [`Revision`].
///
/// While not strictly *necessary* to integrate moxie, much of the runtime's
/// potential value comes from identifying work that's repeated across frames
/// and storing it in the runtime's cache instead of recomputing every time.
///
/// Internally the runtime hosts a [dyn-cache] instance which is
/// garbage-collected at the end of each [`Revision`]. All cached values are
/// stored there and evicted at the end of revisions where they went unused.
/// This behavior also provides deterministic drop timing for values cached by
/// the runtime.
///
/// ## Tasks
///
/// Each runtime expects to be able to spawn futures as async tasks, provided
/// with [`Runtime::set_task_executor`]. By default a no-op spawner is provided.
///
/// # Minimal Example
///
/// This example has no side effects in its root function, and doesn't have any
/// state variables which might require change notifications.
///
/// ```
/// # use moxie::runtime::{Revision, Runtime};
/// let mut rt = Runtime::new();
/// assert_eq!(rt.revision().0, 0);
/// for i in 1..10 {
/// rt.run_once(|| ());
/// assert_eq!(rt.revision(), Revision(i));
/// }
/// ```
///
/// [dyn-cache]: https://docs.rs/dyn-cache
pub struct Runtime {
revision: Revision,
cache: SharedLocalCache,
spawner: Spawner,
wk: Waker,
}
impl Default for Runtime {
fn default() -> Runtime {
Runtime::new()
}
}
impl Runtime {
/// Construct a new [`Runtime`] with blank storage and no external waker or
/// task executor.
pub fn new() -> Self {
Self {
spawner: Spawner(Rc::new(JunkSpawner)),
revision: Revision(0),
cache: SharedLocalCache::default(),
wk: noop_waker(),
}
}
/// The current revision of the runtime, or how many times `run_once` has
/// been invoked.
pub fn revision(&self) -> Revision {
self.revision
}
/// Runs the root closure once with access to the runtime context,
/// increments the runtime's `Revision`, and drops any cached values
/// which were not marked alive.
pub fn run_once<Out>(&mut self, op: impl FnOnce() -> Out) -> Out {
self.revision.0 += 1;
let ret = self.context_handle().offer(|| topo::call(op));
self.cache.gc();
ret
}
/// Sets the [`std::task::Waker`] which will be called when state variables
/// receive commits. By default the runtime no-ops on a state change,
/// which is probably the desired behavior if the embedding system will
/// call `Runtime::run_once` on a regular interval regardless.
pub fn set_state_change_waker(&mut self, wk: Waker) {
self.wk = wk;
}
/// Sets the executor that will be used to spawn normal priority tasks.
pub fn set_task_executor(&mut self, sp: impl LocalSpawn + 'static) {
self.spawner = Spawner(Rc::new(sp));
}
}
#[derive(Clone)]
struct Spawner(pub Rc<dyn LocalSpawn>);
impl Debug for Spawner {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
f.write_fmt(format_args!("{:p}", &self.0))
}
}
struct JunkSpawner;
impl LocalSpawn for JunkSpawner {
fn spawn_local_obj(&self, _: LocalFutureObj<'static, ()>) -> Result<(), SpawnError> {
Err(SpawnError::shutdown())
}
fn status_local(&self) -> Result<(), SpawnError> {
Err(SpawnError::shutdown())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn propagating_env_to_runtime() {
let first_byte = 0u8;
let mut runtime = RunLoop::new(|| {
let from_env: u8 = *illicit::expect();
assert_eq!(from_env, first_byte);
});
assert!(illicit::get::<u8>().is_err());
first_byte.offer(|| {
topo::call(|| runtime.run_once());
});
assert!(illicit::get::<u8>().is_err());
}
#[test]
fn tick_a_few_times() {
let mut rt = RunLoop::new(Revision::current);
assert_eq!(rt.run_once(), Revision(1));
assert_eq!(rt.run_once(), Revision(2));
assert_eq!(rt.run_once(), Revision(3));
assert_eq!(rt.run_once(), Revision(4));
assert_eq!(rt.run_once(), Revision(5));
}
}