Skip to main content

behavior/
compose.rs

1//! Intent-facing typestate composition. Every method immediately builds a
2//! concrete pure behavior; there is no separate intent representation.
3
4use std::time::Duration;
5
6use std::time::Instant;
7
8use crate::Actions;
9use crate::BehaviorBase;
10use crate::behavior::{Address, Behavior, Births};
11use crate::calculus::{BehaviorActed, EventInput, UserEvent, delegate_transition};
12use crate::next::Never;
13use crate::protocol::TimerId;
14use crate::shutdown::{FinalizeOnShutdown, ShutdownReaction, StopOnShutdown};
15use crate::stash::{Stash, StashRoute};
16use crate::supervision::{RestartPolicy, Strategy, SupervisionFailureReaction, Supervisor};
17use crate::timing::{Deadline, DeadlineReaction};
18use crate::timing::{ReceiveTimeout, ReceiveTimeoutReaction};
19use crate::watch::{LinkReaction, Watch};
20
21const DEFAULT_STRATEGY: Strategy = Strategy::OneForOne;
22const DEFAULT_POLICY: RestartPolicy = RestartPolicy::Transient;
23const DEFAULT_BUDGET: (u32, Duration) = (1, Duration::from_secs(5));
24
25/// A behavior definition that may still be wrapped and initialized.
26///
27/// Definitions deliberately expose no mailbox fold:
28///
29/// ```compile_fail
30/// use behavior::{Compose, Machine, MailAddr, Move, Never};
31///
32/// let mut definition: Compose<Machine<MailAddr, (), u8, (), Never>> =
33///     Compose::new(Machine::new((), (), |_, _, _| Ok(Move::Stay)));
34/// definition.receive(MailAddr(0), 1);
35/// ```
36pub struct Compose<B> {
37    behavior: B,
38}
39
40/// An initialized behavior and the effects that must be interpreted before
41/// its first mailbox turn.
42pub struct Initialized<B: Behavior> {
43    pub behavior: Active<B>,
44    pub actions: Actions<B::Addr, B::Ph, B::Sends, B::Birth>,
45}
46
47/// A behavior whose initialization fold has completed exactly once.
48///
49/// `Active<B>` does not implement [`Behavior`], so initialization cannot be
50/// repeated through the public API:
51///
52/// ```compile_fail
53/// use behavior::{Behavior, Compose, Machine, MailAddr, Move, Never};
54///
55/// let definition: Compose<Machine<MailAddr, (), u8, (), Never>> =
56///     Compose::new(Machine::new((), (), |_, _, _| Ok(Move::Stay)));
57/// let active = definition.initialize().unwrap().behavior;
58/// active.initialize();
59/// ```
60pub struct Active<B: Behavior> {
61    pub(crate) behavior: B,
62}
63
64impl<B: Behavior> Active<B> {
65    #[must_use]
66    pub fn base(&self) -> &B::Base
67    where
68        B: BehaviorBase,
69    {
70        self.behavior.base()
71    }
72
73    #[must_use]
74    pub fn stashed(&self) -> usize
75    where
76        B: crate::StashStatus,
77    {
78        self.behavior.stashed_messages()
79    }
80
81    /// Fold exactly one event after initialization.
82    pub fn transition(&mut self, event: B::Event) -> BehaviorActed<B> {
83        delegate_transition(&mut self.behavior, event)
84    }
85
86    /// Fold one user communication after initialization.
87    pub fn receive(&mut self, from: B::Addr, message: B::Msg) -> BehaviorActed<B> {
88        self.transition(B::Event::user(from, message))
89    }
90
91    /// Fold one statically supported semantic input after initialization.
92    pub fn on<Input>(&mut self, input: Input) -> BehaviorActed<B>
93    where
94        B::Event: EventInput<Input>,
95    {
96        self.transition(B::Event::inject(input))
97    }
98}
99
100impl<B: Behavior> core::ops::Deref for Active<B> {
101    type Target = B;
102
103    fn deref(&self) -> &Self::Target {
104        &self.behavior
105    }
106}
107
108impl<B> Compose<B> {
109    /// Begin a composition from the one concrete [`Behavior`] value being
110    /// defined directly or by `#[behavior]`.
111    #[must_use]
112    pub const fn new(behavior: B) -> Self {
113        Self { behavior }
114    }
115}
116
117impl<B: Behavior> Compose<B> {
118    fn map_behavior<Mapped>(self, map: impl FnOnce(B) -> Mapped) -> Compose<Mapped> {
119        Compose {
120            behavior: map(self.behavior),
121        }
122    }
123
124    fn try_map_behavior<Mapped, E>(
125        self,
126        map: impl FnOnce(B) -> Result<Mapped, E>,
127    ) -> Result<Compose<Mapped>, E> {
128        map(self.behavior).map(|behavior| Compose { behavior })
129    }
130
131    #[must_use]
132    pub fn base(&self) -> &B::Base
133    where
134        B: BehaviorBase,
135    {
136        self.behavior.base()
137    }
138
139    #[must_use]
140    pub fn definition(&self) -> &B {
141        &self.behavior
142    }
143
144    /// Consume this definition, perform its one initialization fold, and
145    /// return the active behavior together with the ordered initialization
146    /// effects.
147    ///
148    /// # Errors
149    ///
150    /// Returns the behavior's controlled initialization failure. A failed
151    /// definition is consumed and cannot be activated or retried.
152    pub fn initialize(self) -> Result<Initialized<B>, B::Error> {
153        let mut behavior = self.behavior;
154        let actions = crate::calculus::initialize(&mut behavior)?;
155        Ok(Initialized {
156            behavior: Active { behavior },
157            actions,
158        })
159    }
160
161    /// Stop normally when a typed shutdown request is folded.
162    #[must_use]
163    pub fn stop_on_shutdown(self) -> Compose<StopOnShutdown<B>> {
164        self.map_behavior(StopOnShutdown::new)
165    }
166
167    /// Apply one final pure fold, retain its sends and creations, and stop
168    /// normally regardless of the fold's become verdict.
169    #[must_use]
170    pub fn finalize_on_shutdown(
171        self,
172        finalize: ShutdownReaction<B>,
173    ) -> Compose<FinalizeOnShutdown<B>> {
174        self.map_behavior(|behavior| FinalizeOnShutdown::new(behavior, finalize))
175    }
176
177    /// Observe a peer and apply a pure reaction when it stops.
178    #[must_use]
179    pub fn watch(self, peer: B::Addr, on_stopped: LinkReaction<B>) -> Compose<Watch<B>> {
180        self.map_behavior(|behavior| Watch::new(behavior, peer, on_stopped))
181    }
182
183    /// Apply a pure reaction when the given absolute time is reached.
184    ///
185    #[must_use]
186    pub fn deadline(
187        self,
188        timer: TimerId,
189        when: Option<Instant>,
190        on_reached: DeadlineReaction<B>,
191    ) -> Compose<Deadline<B>> {
192        self.map_behavior(|behavior| Deadline::new(behavior, timer, when, on_reached))
193    }
194
195    /// Notify the behavior once after an idle period containing no successful
196    /// user communication.
197    ///
198    /// Initialization and each successful continuing user fold emit a relative
199    /// schedule. Service events never reset inactivity. A matching delivery is
200    /// consumed before `on_elapsed` runs, and a continuing reaction remains
201    /// unarmed until another successful continuing user communication.
202    ///
203    #[must_use]
204    pub fn receive_timeout(
205        self,
206        timer: TimerId,
207        after: Duration,
208        on_elapsed: ReceiveTimeoutReaction<B>,
209    ) -> Compose<ReceiveTimeout<B>> {
210        self.map_behavior(|behavior| ReceiveTimeout::new(behavior, timer, after, on_elapsed))
211    }
212
213    /// Hold messages selected by `route` and replay them on `Release`.
214    #[must_use]
215    pub fn stash(self, route: fn(&B::Msg) -> StashRoute) -> Compose<Stash<B>>
216    where
217        B: Behavior<Ph = Never>,
218    {
219        self.map_behavior(|behavior| Stash::new(behavior, route))
220    }
221
222    /// Create a supervised child topology with an explicit creator-local
223    /// nonce assignment. Bombay does not infer routing identity from an
224    /// integer position.
225    #[must_use]
226    pub fn children<C>(
227        self,
228        nonces: fn(usize) -> <B::Addr as Address>::Nonce,
229        count: usize,
230        build: fn(usize) -> Option<C>,
231    ) -> Result<Compose<Supervisor<B, C>>, crate::FleetError<<B::Addr as Address>::Nonce>>
232    where
233        B: Behavior<Birth = Births<C>>,
234        C: Behavior<Ph = Never, Addr = B::Addr>,
235        <B::Addr as Address>::Nonce: From<u64>,
236    {
237        self.try_map_behavior(|behavior| {
238            Supervisor::new(
239                behavior,
240                nonces,
241                count,
242                build,
243                DEFAULT_STRATEGY,
244                DEFAULT_POLICY,
245                DEFAULT_BUDGET.0,
246                DEFAULT_BUDGET.1,
247            )
248        })
249    }
250}
251
252impl<B, C> Compose<Supervisor<B, C>>
253where
254    B: Behavior<Birth = Births<C>>,
255    C: Behavior<Ph = Never, Addr = B::Addr>,
256    <B::Addr as Address>::Nonce: From<u64>,
257{
258    #[must_use]
259    pub fn restart(self, strategy: Strategy) -> Self {
260        Self {
261            behavior: self.behavior.with_strategy(strategy),
262        }
263    }
264
265    #[must_use]
266    pub fn when(self, policy: RestartPolicy) -> Self {
267        Self {
268            behavior: self.behavior.with_policy(policy),
269        }
270    }
271
272    #[must_use]
273    pub fn within(self, maximum: u32, window: Duration) -> Self {
274        Self {
275            behavior: self.behavior.with_budget(maximum, window),
276        }
277    }
278
279    /// Apply a pure reaction when supervision can no longer preserve its
280    /// child topology.
281    #[must_use]
282    pub fn on_supervision_failure(self, reaction: SupervisionFailureReaction<B>) -> Self {
283        Self {
284            behavior: self.behavior.with_failure_reaction(reaction),
285        }
286    }
287}