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
//! [`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));
    }
}