Skip to main content

kozan_platform/
time.rs

1//! View-thread async timers — sleep, interval, timeout.
2//!
3//! All three integrate with the [`Scheduler`](kozan_scheduler::Scheduler)'s
4//! timer registry.  Zero threads spawned, zero extra allocations beyond the
5//! sorted registry entry.
6//!
7//! # How the registry drives everything
8//!
9//! ```text
10//! Sleep/Interval/Timeout::poll()     Scheduler::tick()
11//! ──────────────────────────────     ──────────────────────────────────────
12//! register(deadline, waker) ───────► fire_expired()
13//!                                        deadline ≤ now?
14//!                                          yes → waker.wake()
15//!                                               → executor marks task woken
16//!                                               → next poll → Poll::Ready
17//!
18//!                                    calculate_park_timeout()
19//!                                      includes next timer deadline
20//!                                      → thread parks exactly until expiry
21//! ```
22
23use std::future::Future;
24use std::pin::Pin;
25use std::task::{Context, Poll};
26use std::time::{Duration, Instant};
27
28// ── sleep ─────────────────────────────────────────────────────────────────────
29
30/// A future that completes after a given duration.
31///
32/// Created by [`sleep()`].
33pub struct Sleep {
34    deadline: Instant,
35}
36
37impl Future for Sleep {
38    type Output = ();
39
40    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
41        if Instant::now() >= self.deadline {
42            return Poll::Ready(());
43        }
44        kozan_scheduler::timer::register(self.deadline, cx.waker().clone());
45        Poll::Pending
46    }
47}
48
49/// Suspend the current task for `duration` without blocking the view thread.
50///
51/// ```ignore
52/// ctx.spawn(async move {
53///     sleep(Duration::from_millis(500)).await;
54///     card.set_style(activated_style());
55/// });
56/// ```
57#[must_use]
58pub fn sleep(duration: Duration) -> Sleep {
59    Sleep {
60        deadline: Instant::now() + duration,
61    }
62}
63
64// ── interval ──────────────────────────────────────────────────────────────────
65
66/// A periodic timer. Each call to [`tick()`](Interval::tick) returns a future
67/// that resolves at the next scheduled instant.
68///
69/// Deadlines are **fixed-period** (next = prev + period), not floating
70/// (next = now + period), so accumulated drift is O(1) not O(n).
71///
72/// Created by [`interval()`].
73///
74/// ```ignore
75/// ctx.spawn(async move {
76///     let mut frame_timer = interval(Duration::from_millis(16));
77///     loop {
78///         frame_timer.tick().await;
79///         update_animation();
80///     }
81/// });
82/// ```
83pub struct Interval {
84    period: Duration,
85    next_deadline: Instant,
86}
87
88impl Interval {
89    /// Wait until the next tick.
90    ///
91    /// Returns a [`Sleep`] future targeting the pre-computed deadline,
92    /// then advances the deadline by one period.
93    pub fn tick(&mut self) -> Sleep {
94        let deadline = self.next_deadline;
95        self.next_deadline += self.period;
96        Sleep { deadline }
97    }
98}
99
100/// Create a periodic timer that fires every `period`.
101///
102/// The first tick fires after one full `period` (not immediately).
103#[must_use]
104pub fn interval(period: Duration) -> Interval {
105    Interval {
106        period,
107        next_deadline: Instant::now() + period,
108    }
109}
110
111// ── timeout ───────────────────────────────────────────────────────────────────
112
113/// Error returned when a [`timeout`] expires before the wrapped future.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct Elapsed;
116
117impl std::fmt::Display for Elapsed {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(f, "operation timed out")
120    }
121}
122
123impl std::error::Error for Elapsed {}
124
125/// Wraps a future and cancels it if it does not complete within `duration`.
126///
127/// Created by [`timeout()`]. Returns `Ok(value)` if the future wins the race,
128/// or `Err(Elapsed)` if the timer fires first.
129///
130/// # Implementation
131///
132/// Both the inner future and the deadline share the same `Waker`.  Whichever
133/// completes first causes `poll` to be called again, where the winner is
134/// detected and returned.
135///
136/// ```ignore
137/// ctx.spawn(async move {
138///     match timeout(Duration::from_secs(5), fetch_data()).await {
139///         Ok(data) => display(data),
140///         Err(Elapsed) => show_error("Request timed out"),
141///     }
142/// });
143/// ```
144pub struct Timeout<F: Future> {
145    /// The inner future (structurally pinned).
146    future: F,
147    deadline: Instant,
148}
149
150impl<F: Future> Future for Timeout<F> {
151    type Output = Result<F::Output, Elapsed>;
152
153    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
154        // Poll the inner future first (priority: completion over timeout).
155        //
156        // SAFETY: `Timeout` is only used through `Pin` once placed in the
157        // executor's task.  We project the pin to the `future` field, which
158        // is structurally pinned — we never move it out of `Timeout`.
159        let future = unsafe { self.as_mut().map_unchecked_mut(|t| &mut t.future) };
160        if let Poll::Ready(val) = future.poll(cx) {
161            return Poll::Ready(Ok(val));
162        }
163
164        let deadline = self.deadline;
165        if Instant::now() >= deadline {
166            return Poll::Ready(Err(Elapsed));
167        }
168
169        // Register the deadline.  When it fires, the shared waker wakes this
170        // task, we re-poll, and the `Instant::now() >= deadline` check wins.
171        kozan_scheduler::timer::register(deadline, cx.waker().clone());
172        Poll::Pending
173    }
174}
175
176/// Run `future`, but give up and return [`Elapsed`] after `duration`.
177///
178/// ```ignore
179/// let result = timeout(Duration::from_secs(3), expensive_future()).await;
180/// ```
181pub fn timeout<F: Future>(duration: Duration, future: F) -> Timeout<F> {
182    Timeout {
183        future,
184        deadline: Instant::now() + duration,
185    }
186}