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 #[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 #[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 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 let cleared_id = cleared.unwrap();
105
106 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 #[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 let cleared_id = cleared.unwrap();
173 if cleared_id != timer_id {
174 unreachable!("Cleared with the wrong ID");
175 }
176
177 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#[derive(Debug)]
196pub struct TimerHandle {
197 timer_id: TimerId,
198 abort: Sender<TimerId>,
199}
200
201impl TimerHandle {
202 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}