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}