Skip to main content

guinea_core/
feature.rs

1//! The two halves of a reducer's traffic, and the one call that wires them.
2//!
3//! What this replaces is four ways of saying the same thing. Ownership of a
4//! reducer used to be a side effect of which of `port`, `actions`, `wire` or
5//! `seed_reducer` a feature happened to call, and the two ends of one edge -
6//! "this actor drives this reducer" and "this action goes to that actor" -
7//! were declared in different places. A member nobody wired warned at runtime
8//! instead of failing to build.
9//!
10//! Here the pair is known by construction. [`Claim::driven_by`] creates the
11//! actor from the very expression that claims the reducer, and hands the
12//! closure its [`Push`] as a parameter - so the direction is named where it is
13//! used rather than in the name of a method that fetches it. Nothing is left
14//! to wire, so nothing can be left unwired.
15
16use std::rc::Rc;
17
18use crate::actor::event_bus::EventBus;
19use crate::actor::{Addr, ManagedActor, UiThreadToken};
20use crate::scope::{Reducer, Scope};
21
22/// The way back into a reducer, for whoever changes it.
23///
24/// Handed to an actor as a parameter rather than fetched by name. It names its
25/// scope and does not keep it: the scope goes when it is removed, and this
26/// quietly stops.
27pub struct Push<R: Reducer> {
28    scope: Scope,
29    reducer: std::marker::PhantomData<R>,
30}
31
32impl<R: Reducer> Clone for Push<R> {
33    fn clone(&self) -> Self {
34        Self {
35            scope: self.scope,
36            reducer: std::marker::PhantomData,
37        }
38    }
39}
40
41impl<R: Reducer> std::fmt::Debug for Push<R> {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("Push")
44            .field("reducer", &std::any::type_name::<R>())
45            .field("scope_alive", &self.scope.is_alive())
46            .finish()
47    }
48}
49
50impl<R: Reducer> Push<R> {
51    /// The way into `R` in `scope` - for a test that drives an actor without
52    /// installing the feature around it.
53    pub fn new(scope: Scope) -> Self {
54        Self {
55            scope,
56            reducer: std::marker::PhantomData,
57        }
58    }
59
60    /// Applies an update. A no-op once the scope is gone, which is what
61    /// happens to work an actor finishes after its page has been left.
62    pub fn send(&self, update: R::Update) {
63        if self.scope.is_alive() {
64            crate::trace::mark(|| crate::trace::Point::Push {
65                reducer: crate::actor::short_type_name::<R>(),
66            });
67            self.scope.push::<R>(update);
68        }
69    }
70}
71
72/// What the UI may ask of the features a segment can reach.
73///
74/// `emit` takes the action by value, so what is happening is readable where it
75/// happens. Nothing comes back: the answer arrives as an update to a reducer.
76/// That is why everything the UI needs has to be state - "compute this for me
77/// on demand" does not exist - and the same fact is why teardown is safe,
78/// since nobody is ever waiting on something that has gone.
79///
80/// **No actor appears anywhere in this path.** An action is a value the domain
81/// answers; *how* it answers - an actor, a task with a `RefCell`, a channel to
82/// a thread - is the domain's own business and may change without anything
83/// outside noticing. An actor named in a signature would be exactly the leak
84/// this avoids, and the compiler says so out loud: it makes the actor public.
85///
86/// What is still checked at build time is the part that matters: `actor!`
87/// asserts `Handler<M>` for every action it lists, so an action nobody answers
88/// fails to compile *inside the domain*. What is left to run time is whether
89/// the feature is installed here at all - which was never a type's question.
90#[derive(Clone, Default)]
91pub struct Dispatch {
92    /// One installed feature's corner of one scope. Named, not kept: a
93    /// dispatcher a widget captured answers nothing once its page is removed.
94    ///
95    /// One and not a chain: which feature answers is settled by what the
96    /// reader was reading, and reading already found the scope and the
97    /// instance. Searching upward would make two instances of one feature
98    /// indistinguishable again.
99    at: Option<(Scope, usize)>,
100}
101
102impl Dispatch {
103    /// The section that owns `R` - what a reader of `R` is handed.
104    pub fn owning<R: 'static>(scope: Scope) -> Self {
105        Self::in_section(scope, scope.section_of::<R>())
106    }
107
108    /// A named section - what a feature is handed while it is installing.
109    pub(crate) fn in_section(scope: Scope, section: usize) -> Self {
110        Self {
111            at: Some((scope, section)),
112        }
113    }
114
115    /// Hands the action to whatever answers it in that feature.
116    pub fn emit<M: 'static>(&self, action: M) {
117        let found = self
118            .at
119            .and_then(|(scope, section)| scope.answerer::<M>(section));
120
121        match found {
122            Some(answer) => {
123                let _action = crate::trace::enter(|| crate::trace::Point::Action {
124                    message: crate::actor::short_type_name::<M>(),
125                });
126                answer(action)
127            }
128            // The feature that owns what was read does not answer this - a
129            // wiring mistake rather than something a user did, so it says what
130            // went unanswered and lets the frame stand.
131            None => tracing::warn!(
132                action = crate::actor::short_type_name::<M>(),
133                "the feature this dispatcher belongs to does not answer this action; dropped"
134            ),
135        }
136    }
137}
138
139/// The reducers a feature lets other segments read.
140///
141/// `pub` for reducers: listed means a page below can read it, unlisted means
142/// it is the feature's own business. A feature's internal state used to be
143/// reachable from anywhere below simply because it existed, which is the
144/// difference between a feature and a folder.
145///
146/// Implemented for `()` and for tuples of reducers, so a single export is
147/// written `(Processes,)`. The trailing comma is the price of not having a
148/// blanket impl over every reducer: a blanket one would overlap with the tuple
149/// impls, since nothing stops a downstream crate implementing `Reducer` for a
150/// tuple.
151pub trait Exported {
152    fn mark(scope: Scope);
153
154    /// The first listed reducer this scope never claimed, if there is one.
155    ///
156    /// `Installs` closed the drift between what a segment says it installs and
157    /// what it built, by making the list the body's return value. `Exports`
158    /// cannot be a return value - it is read at build time, by
159    /// [`Reaches`](../../guinea_app/feature/trait.Reaches.html), and a value
160    /// arrives too late for that. So the same drift is closed from the other
161    /// end: the list is checked against what was actually claimed, the moment
162    /// the feature finishes installing.
163    ///
164    /// Without it, exporting something the feature never claimed type-checks,
165    /// and a page below reads the reducer's `Default` forever - the state is
166    /// created on first read, so nothing ever fails. Silence is the whole
167    /// problem: a wrong export looks exactly like a feature that has not
168    /// pushed an update yet.
169    fn unclaimed(scope: Scope) -> Option<&'static str>;
170}
171
172fn missing<R: Reducer>(scope: Scope) -> Option<&'static str> {
173    (!scope.claims::<R>()).then(|| std::any::type_name::<R>())
174}
175
176impl Exported for () {
177    fn mark(_scope: Scope) {}
178
179    fn unclaimed(_scope: Scope) -> Option<&'static str> {
180        None
181    }
182}
183
184macro_rules! exported {
185    ($($reducer:ident),+) => {
186        impl<$($reducer: Reducer),+> Exported for ($($reducer,)+) {
187            fn mark(scope: Scope) {
188                $(scope.note_export::<$reducer>();)+
189            }
190
191            fn unclaimed(scope: Scope) -> Option<&'static str> {
192                None$(.or_else(|| missing::<$reducer>(scope)))+
193            }
194        }
195    };
196}
197
198exported!(A);
199exported!(A, B);
200exported!(A, B, C);
201exported!(A, B, C, D);
202exported!(A, B, C, D, E);
203exported!(A, B, C, D, E, F);
204exported!(A, B, C, D, E, F, G);
205exported!(A, B, C, D, E, F, G, H);
206exported!(A, B, C, D, E, F, G, H, I);
207exported!(A, B, C, D, E, F, G, H, I, J);
208exported!(A, B, C, D, E, F, G, H, I, J, K);
209exported!(A, B, C, D, E, F, G, H, I, J, K, L);
210
211/// A domain that answers actions through an actor.
212///
213/// Written by `actor!` from the `handlers { .. }` it already lists, so the
214/// registration cannot drift from the handlers and nothing can be left
215/// unwired. Implementing it by hand is not the alternative to having an actor
216/// - `cx.answers::<M>(..)` is.
217pub trait Serves: Sized + 'static {
218    fn serve(addr: &Addr<Self>, scope: Scope);
219}
220
221/// A reducer being claimed, and what may still be said about it.
222///
223/// Produced by `cx.state::<R>()` during install. Ending the chain without
224/// [`driven_by`](Self::driven_by) is not a half-finished feature - it is state
225/// the UI owns outright, and the type says so: nothing drives it, so nothing
226/// can be emitted to it.
227pub struct Claim<'a, R: Reducer> {
228    scope: Scope,
229    bus: Option<&'a Rc<EventBus>>,
230    token: &'a UiThreadToken,
231    reducer: std::marker::PhantomData<fn() -> R>,
232}
233
234impl<'a, R: Reducer> Claim<'a, R> {
235    /// For a context that hands features their scope - the contexts in
236    /// `guinea-app`, and nothing else. `bus` is the window's, when the scope
237    /// is in one.
238    pub fn new(scope: Scope, bus: Option<&'a Rc<EventBus>>, token: &'a UiThreadToken) -> Self {
239        scope.note_reducer_owner::<R>();
240        Self {
241            scope,
242            bus,
243            token,
244            reducer: std::marker::PhantomData,
245        }
246    }
247
248    /// Starts from something other than `R::default()`.
249    ///
250    /// For a synchronous read - a setting already on disk - so the first frame
251    /// shows real data instead of defaults followed by a round trip.
252    pub fn seed(self, state: R) -> Self {
253        if self.scope.peek::<R>().is_none() {
254            self.scope.seed::<R>(state);
255        }
256        self
257    }
258
259    /// Creates the actor that drives this reducer, and installs it here.
260    ///
261    /// The closure is handed the [`Push`] rather than fetching it, and the
262    /// actor's lifetime becomes the scope's - so a feature declares the whole
263    /// edge in one expression and owns none of the bookkeeping. The actor type
264    /// is inferred from what the closure returns; neither the reducer nor this
265    /// call has to name it.
266    ///
267    /// The address comes back for wiring that is the actor's own, such as
268    /// `addr.subscribe_on::<M>(Bus::Global)`. The scope still owns the actor
269    /// and disposes it, and what it subscribed to ends with it.
270    pub fn driven_by<A, F>(self, build: F) -> (Bound<R>, Addr<A>)
271    where
272        F: FnOnce(Push<R>) -> A,
273        A: ManagedActor + Serves + std::fmt::Debug + 'static,
274    {
275        let actor = Addr::new_managed_scoped(build(Push::new(self.scope)), self.token.clone());
276        actor.live_in(self.scope, self.bus);
277        A::serve(&actor, self.scope);
278        self.scope.hold_actor(
279            &actor,
280            self.scope.current_feature(),
281            Some(crate::actor::short_type_name::<R>()),
282        );
283        self.scope.own(actor.clone());
284        (self.bound(), actor)
285    }
286
287    /// Leaves it for the feature to drive however it likes.
288    ///
289    /// State the UI owns outright, or a domain that answers with something
290    /// other than an actor - see `cx.answers::<M>(..)`.
291    pub fn plain(self) -> Bound<R> {
292        self.bound()
293    }
294
295    fn bound(self) -> Bound<R> {
296        Bound {
297            push: Push::new(self.scope),
298            // The section being installed: a feature's own handle answers
299            // through itself, not through whatever else the scope holds.
300            dispatch: Dispatch::in_section(self.scope, self.scope.current_section()),
301        }
302    }
303}
304
305/// What a feature keeps of a reducer it claimed.
306///
307/// Both directions, because a feature is the one place that legitimately has
308/// both: it may change the state itself and it may ask its actor for
309/// something. Cheap to hold - two handles, no state.
310pub struct Bound<R: Reducer> {
311    push: Push<R>,
312    dispatch: Dispatch,
313}
314
315impl<R: Reducer> Clone for Bound<R> {
316    fn clone(&self) -> Self {
317        Self {
318            push: self.push.clone(),
319            dispatch: self.dispatch.clone(),
320        }
321    }
322}
323
324impl<R: Reducer> Bound<R> {
325    pub fn push(&self, update: R::Update) {
326        self.push.send(update);
327    }
328
329    pub fn emit<M: 'static>(&self, action: M) {
330        self.dispatch.emit(action);
331    }
332
333    /// The way in, for handing to something that is not an actor - a timer, a
334    /// stream, a callback from a library.
335    pub fn port(&self) -> Push<R> {
336        self.push.clone()
337    }
338}