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` is
158    /// a type the feature names rather than a value it returns, so the same
159    /// drift is closed from the other end: the list is checked against what
160    /// was actually claimed, the moment the feature finishes installing.
161    ///
162    /// Without it, exporting something the feature never claimed type-checks,
163    /// and a page below reads the reducer's `Default` forever - the state is
164    /// created on first read, so nothing ever fails. Silence is the whole
165    /// problem: a wrong export looks exactly like a feature that has not
166    /// pushed an update yet.
167    fn unclaimed(scope: Scope) -> Option<&'static str>;
168
169    /// The listed reducers, for a check that reads a route tree without
170    /// installing it.
171    fn named(into: &mut Vec<Named>);
172}
173
174/// A type as a check reads it: what tells it apart, and what to call it.
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176pub struct Named {
177    pub id: std::any::TypeId,
178    pub name: &'static str,
179}
180
181impl Named {
182    pub fn of<T: 'static>() -> Self {
183        Self {
184            id: std::any::TypeId::of::<T>(),
185            name: std::any::type_name::<T>(),
186        }
187    }
188}
189
190fn missing<R: Reducer>(scope: Scope) -> Option<&'static str> {
191    (!scope.claims::<R>()).then(|| std::any::type_name::<R>())
192}
193
194impl Exported for () {
195    fn mark(_scope: Scope) {}
196
197    fn unclaimed(_scope: Scope) -> Option<&'static str> {
198        None
199    }
200
201    fn named(_into: &mut Vec<Named>) {}
202}
203
204macro_rules! exported {
205    ($($reducer:ident),+) => {
206        impl<$($reducer: Reducer),+> Exported for ($($reducer,)+) {
207            fn mark(scope: Scope) {
208                $(scope.note_export::<$reducer>();)+
209            }
210
211            fn unclaimed(scope: Scope) -> Option<&'static str> {
212                None$(.or_else(|| missing::<$reducer>(scope)))+
213            }
214
215            fn named(into: &mut Vec<Named>) {
216                $(into.push(Named::of::<$reducer>());)+
217            }
218        }
219    };
220}
221
222exported!(A);
223exported!(A, B);
224exported!(A, B, C);
225exported!(A, B, C, D);
226exported!(A, B, C, D, E);
227exported!(A, B, C, D, E, F);
228exported!(A, B, C, D, E, F, G);
229exported!(A, B, C, D, E, F, G, H);
230exported!(A, B, C, D, E, F, G, H, I);
231exported!(A, B, C, D, E, F, G, H, I, J);
232exported!(A, B, C, D, E, F, G, H, I, J, K);
233exported!(A, B, C, D, E, F, G, H, I, J, K, L);
234
235/// A domain that answers actions through an actor.
236///
237/// Written by `actor!` from the `handlers { .. }` it already lists, so the
238/// registration cannot drift from the handlers and nothing can be left
239/// unwired. Implementing it by hand is not the alternative to having an actor
240/// - `cx.answers::<M>(..)` is.
241pub trait Serves: Sized + 'static {
242    fn serve(addr: &Addr<Self>, scope: Scope);
243}
244
245/// A reducer being claimed, and what may still be said about it.
246///
247/// Produced by `cx.state::<R>()` during install. Ending the chain without
248/// [`driven_by`](Self::driven_by) is not a half-finished feature - it is state
249/// the UI owns outright, and the type says so: nothing drives it, so nothing
250/// can be emitted to it.
251pub struct Claim<'a, R: Reducer> {
252    scope: Scope,
253    bus: Option<&'a Rc<EventBus>>,
254    token: &'a UiThreadToken,
255    reducer: std::marker::PhantomData<fn() -> R>,
256}
257
258impl<'a, R: Reducer> Claim<'a, R> {
259    /// For a context that hands features their scope - the contexts in
260    /// `guinea-app`, and nothing else. `bus` is the window's, when the scope
261    /// is in one.
262    pub fn new(scope: Scope, bus: Option<&'a Rc<EventBus>>, token: &'a UiThreadToken) -> Self {
263        scope.note_reducer_owner::<R>();
264        Self {
265            scope,
266            bus,
267            token,
268            reducer: std::marker::PhantomData,
269        }
270    }
271
272    /// Starts from something other than `R::default()`.
273    ///
274    /// For a synchronous read - a setting already on disk - so the first frame
275    /// shows real data instead of defaults followed by a round trip.
276    pub fn seed(self, state: R) -> Self {
277        if self.scope.peek::<R>().is_none() {
278            self.scope.seed::<R>(state);
279        }
280        self
281    }
282
283    /// Creates the actor that drives this reducer, and installs it here.
284    ///
285    /// The closure is handed the [`Push`] rather than fetching it, and the
286    /// actor's lifetime becomes the scope's - so a feature declares the whole
287    /// edge in one expression and owns none of the bookkeeping. The actor type
288    /// is inferred from what the closure returns; neither the reducer nor this
289    /// call has to name it.
290    ///
291    /// The address comes back for wiring that is the actor's own, such as
292    /// `addr.subscribe_on::<M>(Bus::Global)`. The scope still owns the actor
293    /// and disposes it, and what it subscribed to ends with it.
294    pub fn driven_by<A, F>(self, build: F) -> (Bound<R>, Addr<A>)
295    where
296        F: FnOnce(Push<R>) -> A,
297        A: ManagedActor + Serves + std::fmt::Debug + 'static,
298    {
299        let actor = Addr::new_managed(build(Push::new(self.scope)), self.token.clone());
300        actor.live_in(self.scope, self.bus);
301        A::serve(&actor, self.scope);
302        self.scope.hold_actor(
303            &actor,
304            self.scope.current_feature(),
305            Some(crate::actor::short_type_name::<R>()),
306        );
307        self.scope.own(actor.clone());
308        (self.bound(), actor)
309    }
310
311    /// Leaves it for the feature to drive however it likes.
312    ///
313    /// State the UI owns outright, or a domain that answers with something
314    /// other than an actor - see `cx.answers::<M>(..)`.
315    pub fn plain(self) -> Bound<R> {
316        self.bound()
317    }
318
319    fn bound(self) -> Bound<R> {
320        Bound {
321            push: Push::new(self.scope),
322            // The section being installed: a feature's own handle answers
323            // through itself, not through whatever else the scope holds.
324            dispatch: Dispatch::in_section(self.scope, self.scope.current_section()),
325        }
326    }
327}
328
329/// What a feature keeps of a reducer it claimed.
330///
331/// Both directions, because a feature is the one place that legitimately has
332/// both: it may change the state itself and it may ask its actor for
333/// something. Cheap to hold - two handles, no state.
334pub struct Bound<R: Reducer> {
335    push: Push<R>,
336    dispatch: Dispatch,
337}
338
339impl<R: Reducer> Clone for Bound<R> {
340    fn clone(&self) -> Self {
341        Self {
342            push: self.push.clone(),
343            dispatch: self.dispatch.clone(),
344        }
345    }
346}
347
348impl<R: Reducer> Bound<R> {
349    pub fn push(&self, update: R::Update) {
350        self.push.send(update);
351    }
352
353    pub fn emit<M: 'static>(&self, action: M) {
354        self.dispatch.emit(action);
355    }
356
357    /// The way in, for handing to something that is not an actor - a timer, a
358    /// stream, a callback from a library.
359    pub fn port(&self) -> Push<R> {
360        self.push.clone()
361    }
362}