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}