crux_time/
command.rs

1use std::{
2    future::Future,
3    marker::PhantomData,
4    time::{Duration, SystemTime},
5};
6
7use crux_core::{command::RequestBuilder, Command, Request};
8use futures::{
9    channel::oneshot::{self, Sender},
10    select_biased, FutureExt,
11};
12
13use crate::{get_timer_id, TimeRequest, TimeResponse, TimerId};
14
15/// Result of the timer run. Timers can either run to completion or be cleared early.
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub enum TimerOutcome {
18    /// Timer completed successfully.
19    Completed(CompletedTimerHandle),
20    /// Timer was cleared early.
21    Cleared,
22}
23
24pub struct Time<Effect, Event> {
25    // Allow impl level trait bounds to avoid repetition
26    effect: PhantomData<Effect>,
27    event: PhantomData<Event>,
28}
29
30impl<Effect, Event> Time<Effect, Event>
31where
32    Effect: Send + From<Request<TimeRequest>> + 'static,
33    Event: Send + 'static,
34{
35    /// Ask for the current wall-clock time.
36    /// # Panics
37    /// Panics if the response is not `TimeResponse::Now`.
38    #[must_use]
39    pub fn now() -> RequestBuilder<Effect, Event, impl Future<Output = SystemTime>> {
40        Command::request_from_shell(TimeRequest::Now).map(|r| {
41            let TimeResponse::Now { instant } = r else {
42                panic!("Incorrect response received for TimeRequest::Now")
43            };
44
45            instant.into()
46        })
47    }
48
49    /// Ask to receive a notification when the specified
50    /// [`SystemTime`] has arrived.
51    /// # Panics
52    /// Panics if the response is not `TimeResponse::InstantArrived`
53    /// or if the timer ID is not the same as the one used to create the handle.
54    #[must_use]
55    pub fn notify_at(
56        system_time: SystemTime,
57    ) -> (
58        RequestBuilder<Effect, Event, impl Future<Output = TimerOutcome>>,
59        TimerHandle,
60    ) {
61        let timer_id = get_timer_id();
62        let (sender, mut receiver) = oneshot::channel();
63
64        let handle = TimerHandle {
65            timer_id,
66            abort: sender,
67        };
68
69        let completed_handle = CompletedTimerHandle { timer_id };
70
71        // The `panic`s in the body of the builder would be `unreachable`s in Rust,
72        // but since the shell is involved we can't check for them statically. Either way,
73        // they are a developer error and suggest something quite wrong with the time implementation
74        // in the shell.
75        let builder = RequestBuilder::new(move |ctx| {
76            async move {
77                if let Ok(Some(cleared_id)) = receiver.try_recv() {
78                    if cleared_id == timer_id {
79                        return TimerOutcome::Cleared;
80                    }
81                }
82
83                select_biased! {
84                    response = ctx.request_from_shell(
85                        TimeRequest::NotifyAt {
86                            id: timer_id,
87                            instant: system_time.into()
88                        }
89                    ).fuse() =>  {
90                        let TimeResponse::InstantArrived { id } = response else {
91                            panic!("Unexpected response to TimeRequest::NotifyAt");
92                        };
93
94                        assert!(id == timer_id, "InstantArrived with unexpected timer ID");
95
96                        TimerOutcome::Completed(completed_handle)
97                    },
98                    cleared = receiver => {
99                        // The Err variant would mean the sender was dropped,
100                        // but `receiver` is a fused future,
101                        // which signals `is_terminated` true in that case,
102                        // so this branch of the select will
103                        // never run for the Err case
104                        let cleared_id = cleared.unwrap();
105
106                        // Follow up by asking the shell to clear the timer
107                        let TimeResponse::Cleared { id } = ctx.request_from_shell(TimeRequest::Clear { id: cleared_id }).await else {
108                            panic!("Unexpected response to TimeRequest::Clear");
109                        };
110
111                        assert!(id == cleared_id, "Cleared with unexpected timer ID");
112
113                        TimerOutcome::Cleared
114                    }
115                }
116            }
117        });
118
119        (builder, handle)
120    }
121
122    /// Ask to receive a notification after the specified
123    /// [`Duration`] has elapsed.
124    /// # Panics
125    /// Panics if the response is not `TimeResponse::DurationElapsed`
126    /// or if the timer ID is not the same as the one used to create the handle.
127    #[must_use]
128    pub fn notify_after(
129        duration: Duration,
130    ) -> (
131        RequestBuilder<Effect, Event, impl Future<Output = TimerOutcome>>,
132        TimerHandle,
133    ) {
134        let timer_id = get_timer_id();
135        let (sender, mut receiver) = oneshot::channel();
136
137        let handle = TimerHandle {
138            timer_id,
139            abort: sender,
140        };
141
142        let completed_handle = CompletedTimerHandle { timer_id };
143
144        let builder = RequestBuilder::new(move |ctx| async move {
145            if let Ok(Some(cleared_id)) = receiver.try_recv() {
146                if cleared_id == timer_id {
147                    return TimerOutcome::Cleared;
148                }
149            }
150
151            select_biased! {
152                response = ctx.request_from_shell(
153                    TimeRequest::NotifyAfter {
154                        id: timer_id,
155                        duration: duration.into()
156                    }
157                ).fuse() => {
158                    let TimeResponse::DurationElapsed { id } = response else {
159                        panic!("Unexpected response to TimeRequest::NotifyAt");
160                    };
161
162                    assert!(id == timer_id, "InstantArrived with unexpected timer ID");
163
164                    TimerOutcome::Completed(completed_handle)
165                }
166                cleared = receiver => {
167                    // The Err variant would mean the sender was dropped,
168                    // but `receiver` is a fused future,
169                    // which signals `is_terminated` true in that case,
170                    // so this branch of the select will
171                    // never run for the Err case
172                    let cleared_id = cleared.unwrap();
173                    if cleared_id != timer_id {
174                        unreachable!("Cleared with the wrong ID");
175                    }
176
177                    // Follow up by asking the shell to clear the timer
178                    let TimeResponse::Cleared { id } = ctx.request_from_shell(TimeRequest::Clear { id: cleared_id }).await else {
179                        panic!("Unexpected response to TimeRequest::Clear");
180                    };
181
182                    assert!(id == cleared_id, "Cleared resolved with unexpected timer ID");
183
184                    TimerOutcome::Cleared
185                }
186            }
187        });
188
189        (builder, handle)
190    }
191}
192
193/// A handle to a requested timer. Allows the timer to be cleared. The handle is safe to drop,
194/// in which case the original timer is no longer abortable
195#[derive(Debug)]
196pub struct TimerHandle {
197    timer_id: TimerId,
198    abort: Sender<TimerId>,
199}
200
201impl TimerHandle {
202    /// Clear the associated timer request.
203    /// The shell will be notified that the timer has been cleared
204    /// with `TimeRequest::Clear { id }`,
205    /// so it can clean up associated resources.
206    /// The original task will resolve
207    /// with `TimeResponse::Cleared { id }`.
208    pub fn clear(self) {
209        let _ = self.abort.send(self.timer_id);
210    }
211}
212
213#[derive(Debug, PartialEq, Eq, Clone)]
214pub struct CompletedTimerHandle {
215    timer_id: TimerId,
216}
217
218impl Eq for TimerHandle {}
219
220impl PartialEq for TimerHandle {
221    fn eq(&self, other: &Self) -> bool {
222        self.timer_id == other.timer_id
223    }
224}
225
226impl PartialEq<CompletedTimerHandle> for TimerHandle {
227    fn eq(&self, other: &CompletedTimerHandle) -> bool {
228        self.timer_id == other.timer_id
229    }
230}
231
232impl PartialEq<TimerHandle> for CompletedTimerHandle {
233    fn eq(&self, other: &TimerHandle) -> bool {
234        self.timer_id == other.timer_id
235    }
236}
237
238impl From<TimerHandle> for CompletedTimerHandle {
239    fn from(value: TimerHandle) -> Self {
240        Self {
241            timer_id: value.timer_id,
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use std::time::Duration;
249
250    use crux_core::Request;
251
252    use super::{Time, TimerOutcome};
253    use crate::{TimeRequest, TimeResponse};
254
255    enum Effect {
256        Time(Request<TimeRequest>),
257    }
258
259    impl From<Request<TimeRequest>> for Effect {
260        fn from(value: Request<TimeRequest>) -> Self {
261            Self::Time(value)
262        }
263    }
264
265    #[derive(Debug, PartialEq)]
266    enum Event {
267        Elapsed(TimerOutcome),
268    }
269
270    #[test]
271    fn timer_can_be_cleared() {
272        let (cmd, handle) = Time::notify_after(Duration::from_secs(2));
273        let mut cmd = cmd.then_send(Event::Elapsed);
274
275        let effect = cmd.effects().next();
276
277        assert!(cmd.events().next().is_none());
278
279        let Some(Effect::Time(_request)) = effect else {
280            panic!("should get an effect");
281        };
282
283        handle.clear();
284
285        let effect = cmd.effects().next();
286        assert!(cmd.events().next().is_none());
287
288        let Some(Effect::Time(mut request)) = effect else {
289            panic!("should get an effect");
290        };
291
292        let TimeRequest::Clear { id } = request.operation else {
293            panic!("expected a Clear request");
294        };
295
296        request
297            .resolve(TimeResponse::Cleared { id })
298            .expect("should resolve");
299
300        let event = cmd.events().next();
301
302        assert!(matches!(event, Some(Event::Elapsed(TimerOutcome::Cleared))));
303    }
304
305    #[test]
306    fn dropping_a_timer_handle_does_not_clear_the_request() {
307        let (cmd, handle) = Time::notify_after(Duration::from_secs(2));
308        drop(handle);
309
310        let mut cmd = cmd.then_send(Event::Elapsed);
311        let effect = cmd.effects().next();
312
313        assert!(cmd.events().next().is_none());
314
315        let Some(Effect::Time(mut request)) = effect else {
316            panic!("should get an effect");
317        };
318
319        let TimeRequest::NotifyAfter { id, .. } = request.operation else {
320            panic!("Expected a NotifyAfter");
321        };
322
323        request
324            .resolve(TimeResponse::DurationElapsed { id })
325            .expect("should resolve");
326
327        let event = cmd.events().next();
328
329        assert!(matches!(
330            event,
331            Some(Event::Elapsed(TimerOutcome::Completed(_)))
332        ));
333    }
334
335    #[test]
336    fn dropping_a_timer_request_while_holding_a_handle_and_polling() {
337        let (cmd, handle) = Time::notify_after(Duration::from_secs(2));
338        let mut cmd = cmd.then_send(Event::Elapsed);
339
340        let effect: Effect = cmd.effects().next().expect("Expected an effect!");
341
342        drop(effect);
343        assert!(!cmd.is_done());
344
345        drop(handle);
346        assert!(cmd.is_done());
347    }
348}