Skip to main content

canic_core/api/
timer.rs

1//! Module: api::timer
2//!
3//! Responsibility: public timer facade for macro-expanded lifecycle code.
4//! Does not own: timer persistence, lifecycle hooks, or scheduling policy.
5//! Boundary: delegates timer creation and clearing to runtime timer workflow.
6
7use crate::workflow::runtime::timer::{TimerId, TimerWorkflow};
8use std::{cell::RefCell, future::Future, rc::Rc, thread::LocalKey, time::Duration};
9
10///
11/// TimerHandle
12///
13/// Opaque handle for scheduled timers (no direct access to TimerId).
14///
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct TimerHandle(TimerId);
18
19///
20/// TimerSlot
21///
22/// Opaque timer slot handle for guarded scheduling.
23///
24
25pub type TimerSlot = LocalKey<RefCell<Option<TimerHandle>>>;
26
27///
28/// TimerApi
29///
30/// Lifecycle timer api façade for macro-expanded entrypoints.
31///
32
33pub struct TimerApi;
34
35impl TimerApi {
36    /// Public, stable surface for macro-expanded code in downstream crates.
37    /// Performs no logic; delegates to internal TimerOps.
38    pub fn set_lifecycle_timer(
39        delay: Duration,
40        label: impl Into<String>,
41        task: impl Future<Output = ()> + 'static,
42    ) -> TimerHandle {
43        TimerHandle(TimerWorkflow::set(delay, label, task))
44    }
45
46    /// Schedule a one-shot timer only if the slot is empty.
47    /// Returns true when a new timer was scheduled.
48    pub fn set_guarded(
49        slot: &'static TimerSlot,
50        delay: Duration,
51        label: impl Into<String>,
52        task: impl Future<Output = ()> + 'static,
53    ) -> bool {
54        slot.with_borrow_mut(|entry| {
55            if entry.is_some() {
56                return false;
57            }
58
59            let id = TimerWorkflow::set(delay, label, task);
60            *entry = Some(TimerHandle(id));
61            true
62        })
63    }
64
65    /// Schedule a repeating timer. Task produces a fresh Future on each tick.
66    pub fn set_interval<F, Fut>(
67        interval: Duration,
68        label: impl Into<String>,
69        task: F,
70    ) -> TimerHandle
71    where
72        F: FnMut() -> Fut + 'static,
73        Fut: Future<Output = ()> + 'static,
74    {
75        TimerHandle(TimerWorkflow::set_interval(interval, label, task))
76    }
77
78    /// Schedule a guarded init timer that installs a repeating interval timer.
79    pub fn set_guarded_interval<FInit, InitFut, FTick, TickFut>(
80        slot: &'static TimerSlot,
81        init_delay: Duration,
82        init_label: impl Into<String>,
83        init_task: FInit,
84        interval: Duration,
85        interval_label: impl Into<String>,
86        tick_task: FTick,
87    ) -> bool
88    where
89        FInit: FnOnce() -> InitFut + 'static,
90        InitFut: Future<Output = ()> + 'static,
91        FTick: FnMut() -> TickFut + 'static,
92        TickFut: Future<Output = ()> + 'static,
93    {
94        let init_label = init_label.into();
95        let interval_label = interval_label.into();
96
97        slot.with_borrow_mut(|entry| {
98            if entry.is_some() {
99                return false;
100            }
101
102            let init_id_cell = Rc::new(RefCell::new(None));
103            let init_id_cell_task = Rc::clone(&init_id_cell);
104
105            let init_id = TimerWorkflow::set(init_delay, init_label, async move {
106                init_task().await;
107
108                let init_id = init_id_cell_task.borrow();
109                let Some(init_id) = init_id.as_ref() else {
110                    return;
111                };
112
113                let still_armed = slot.with_borrow(|slot_val| slot_val.as_ref() == Some(init_id));
114                if !still_armed {
115                    return;
116                }
117
118                let interval_id = TimerWorkflow::set_interval(interval, interval_label, tick_task);
119                let interval_handle = TimerHandle(interval_id);
120
121                // Atomically replace the slot value and clear the previous timer id.
122                // This prevents orphaned timers if callers clear around the handover.
123                slot.with_borrow_mut(|slot_val| {
124                    let old = slot_val.replace(interval_handle);
125                    if let Some(old_id) = old
126                        && old_id != interval_handle
127                    {
128                        TimerWorkflow::clear(old_id.0);
129                    }
130                });
131            });
132
133            let init_handle = TimerHandle(init_id);
134            *init_id_cell.borrow_mut() = Some(init_handle);
135            *entry = Some(init_handle);
136
137            true
138        })
139    }
140}