Skip to main content

myrmic_sdk/host_functions/cell/
timers.rs

1use core::ffi::c_int;
2use core::time::Duration;
3
4use myrmic_common::cells::{Command, CreateTimerRequest};
5
6use crate::error::{ApiError, ErrorCode};
7use crate::{ApiResult, Callback, Void};
8
9mod c_functions {
10    use core::ffi::c_int;
11
12    #[link(wasm_import_module = "cell")]
13    unsafe extern "C" {
14
15        /// Creates a timer (periodic interval or one-shot delay). The payload is a
16        /// serialized `CreateTimerRequest` specifying the export name and schedule.
17        ///
18        /// # Arguments:
19        /// - `buffer`: pointer to the serialized `CreateTimerRequest`
20        /// - `length`: length of the serialized request
21        ///
22        /// # Returns:
23        /// - timer ID (>= 0) on success
24        /// - negative error code on failure
25        pub(super) fn create_timer(buffer: *const u8, length: c_int) -> c_int;
26
27        /// Cancels an active timer.
28        ///
29        /// # Arguments:
30        /// - `id`: the timer ID returned by `create_timer`
31        ///
32        /// # Returns:
33        /// - [`crate::SUCCESS`] on success
34        /// - negative error code on failure
35        pub(super) fn cancel_timer(id: c_int) -> c_int;
36    }
37}
38
39/// Handle to an active interval or delay. Can be cancelled by calling `.cancel()`.
40///
41/// **Important:** Dropping the handle does NOT cancel the timer — the timer
42/// continues running on the host. To retain the ability to cancel, store the
43/// handle in cell state so it persists across handler invocations.
44#[must_use = "dropping the handle loses the ability to cancel the timer — store it in cell state"]
45#[derive(serde::Serialize, serde::Deserialize)]
46pub struct TimerHandle {
47    id: u32,
48}
49
50impl TimerHandle {
51    /// Cancels the timer, consuming the handle.
52    pub fn cancel(self) -> ApiResult<()> {
53        // SAFETY: Wasm linear memory is isolated — the host is responsible for
54        // correct handling of the timer ID.
55        unsafe { c_functions::cancel_timer(self.id as c_int) }.to_result()
56    }
57}
58
59/// Creates a periodic interval that calls the named export on each tick.
60#[must_use]
61pub fn interval(cb: Callback<Void>, period: Duration) -> IntervalBuilder {
62    IntervalBuilder {
63        export_name: cb.into(),
64        delay: None,
65        period,
66        count: None,
67        fixed_delay: false,
68    }
69}
70
71/// Creates a periodic interval with an initial delay before the first tick.
72#[must_use]
73pub fn interval_at(cb: Callback<Void>, delay: Duration, period: Duration) -> IntervalBuilder {
74    IntervalBuilder {
75        export_name: cb.into(),
76        delay: Some(delay),
77        period,
78        count: None,
79        fixed_delay: false,
80    }
81}
82
83/// Creates a one-shot delayed action that calls the named export after the delay.
84#[must_use]
85pub fn delay(cb: Callback<Void>, delay: Duration) -> DelayBuilder {
86    DelayBuilder {
87        export_name: cb.into(),
88        delay,
89    }
90}
91
92/// Builder for periodic intervals. Supports optional `.count()` for finite repetition.
93pub struct IntervalBuilder {
94    export_name: Command,
95    delay: Option<Duration>,
96    period: Duration,
97    count: Option<u32>,
98    fixed_delay: bool,
99}
100
101impl IntervalBuilder {
102    /// Limits the interval to a finite number of ticks.
103    #[must_use]
104    pub fn count(mut self, count: u32) -> Self {
105        self.count = Some(count);
106        self
107    }
108
109    /// Schedules the next interval tick after the previous exported handler returns.
110    #[must_use]
111    pub fn fixed_delay(mut self) -> Self {
112        self.fixed_delay = true;
113        self
114    }
115
116    /// Creates the interval and returns a handle.
117    pub fn build(self) -> ApiResult<TimerHandle> {
118        if self.count == Some(0) {
119            let _ = crate::warn!("timer count of 0 is invalid — timer would never fire");
120            return Err(ApiError::Usage);
121        }
122        let request = CreateTimerRequest {
123            export_name: self.export_name.as_ref().into(),
124            delay_ms: self.delay.map_or(0, |d| d.as_millis() as u64),
125            period_ms: self.period.as_millis() as u64,
126            count: self.count,
127            fixed_delay: self.fixed_delay,
128        };
129        send_create_request(request)
130    }
131}
132
133/// Builder for one-shot delayed actions.
134pub struct DelayBuilder {
135    export_name: Command,
136    delay: Duration,
137}
138
139impl DelayBuilder {
140    /// Creates the delay and returns a handle.
141    pub fn build(self) -> ApiResult<TimerHandle> {
142        let request = CreateTimerRequest {
143            export_name: self.export_name.as_ref().into(),
144            delay_ms: self.delay.as_millis() as u64,
145            period_ms: 0,
146            count: Some(1),
147            fixed_delay: false,
148        };
149        send_create_request(request)
150    }
151}
152
153fn send_create_request(request: CreateTimerRequest) -> ApiResult<TimerHandle> {
154    let bytes = postcard::to_allocvec(&request).expect("serialization should not fail");
155    // SAFETY: Wasm linear memory is isolated — the host reads from the
156    // provided pointer/length and is responsible for correct behaviour.
157    let id = unsafe { c_functions::create_timer(bytes.as_ptr(), bytes.len() as c_int) };
158    if id >= 0 {
159        Ok(TimerHandle { id: id as u32 })
160    } else {
161        Err(id.into())
162    }
163}