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#[derive(Debug, PartialEq, Eq, Clone)]
17pub enum TimerOutcome {
18 Completed(CompletedTimerHandle),
20 Cleared,
22}
23
24pub struct Time<Effect, Event> {
25 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 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 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 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 let cleared_id = cleared.unwrap();
100
101 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 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 let cleared_id = cleared.unwrap();
168 if cleared_id != timer_id {
169 unreachable!("Cleared with the wrong ID");
170 }
171
172 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#[derive(Debug)]
193pub struct TimerHandle {
194 timer_id: TimerId,
195 abort: Sender<TimerId>,
196}
197
198impl TimerHandle {
199 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}