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