timed_fsm/dispatch.rs
1use std::time::Duration;
2
3use crate::response::{Response, TimerCommand};
4
5/// A runtime that can set and kill timers.
6///
7/// The state machine produces [`TimerCommand`]s in its [`Response`].
8/// The runtime implements this trait to translate those commands into
9/// actual platform timer operations.
10///
11/// # Invariant: `Set` resets an existing timer
12///
13/// Implementations **must** ensure that calling [`set_timer`](Self::set_timer)
14/// with an ID that already has an active timer **replaces** that timer rather
15/// than creating a second concurrent timer for the same ID. The new timer
16/// starts counting from the moment `set_timer` is called.
17///
18/// # Platform examples
19///
20/// | Platform | `set_timer` | `kill_timer` |
21/// |----------|-------------|--------------|
22/// | Windows | `SetTimer()` (reuses the same `nIDEvent`) | `KillTimer()` |
23/// | Linux | `timerfd_settime()` (overwrites existing armed state) | `timerfd_settime(0)` |
24/// | macOS | `CFRunLoopTimerSetNextFireDate()` (adjusts fire date) | invalidate the `CFRunLoopTimerRef` |
25/// | Test | record to `Vec` | record to `Vec` |
26pub trait TimerRuntime {
27 /// The timer identifier type (must match the state machine's `TimerId`).
28 type TimerId;
29
30 /// Start or restart a timer.
31 ///
32 /// If a timer with the same ID is already active, it **must** be reset
33 /// to the new duration — only one timer per ID may be active at a time.
34 fn set_timer(&mut self, id: Self::TimerId, duration: Duration);
35
36 /// Stop an active timer. No-op if the timer is not active.
37 fn kill_timer(&mut self, id: Self::TimerId);
38}
39
40/// A runtime that can execute actions produced by the state machine.
41///
42/// # Order guarantee
43///
44/// The `actions` slice passed to [`execute`](Self::execute) is always in the
45/// same order as they appear in [`Response::actions`](crate::Response::actions).
46/// Implementations **must** process them in order (first to last) to preserve
47/// the intended sequence of output events (e.g., modifier-down before
48/// character-down in a key injection scenario).
49///
50/// # Platform examples
51///
52/// | Platform | Implementation |
53/// |----------|----------------|
54/// | Windows | `SendInput()` for key injection |
55/// | Linux | `uinput` write |
56/// | macOS | `CGEventPost()` |
57/// | Test | record to `Vec` |
58pub trait ActionExecutor {
59 /// The action type (must match the state machine's `Action`).
60 type Action;
61
62 /// Execute a batch of actions, in the order they appear in the slice.
63 fn execute(&mut self, actions: &[Self::Action]);
64}
65
66/// Dispatch a [`Response`] to a runtime.
67///
68/// This is the primary integration point between the state machine
69/// (which is pure and side-effect-free) and the runtime (which
70/// performs actual I/O).
71///
72/// # Processing order
73///
74/// 1. **Timer commands** — all [`TimerCommand`]s in `response.timers`
75/// are processed in order via [`TimerRuntime::set_timer`] /
76/// [`TimerRuntime::kill_timer`].
77/// 2. **Actions** — if `response.actions` is non-empty,
78/// [`ActionExecutor::execute`] is called once with the full slice.
79///
80/// Timer commands are applied before actions so that a newly started
81/// timer cannot fire before the actions from the same transition have
82/// been executed (in single-threaded runtimes). In multi-threaded
83/// runtimes, callers are responsible for any necessary synchronization.
84///
85/// Returns the `consumed` flag from the response.
86///
87/// # Example
88///
89/// ```
90/// use std::time::Duration;
91/// use timed_fsm::{Response, dispatch, TimerRuntime, ActionExecutor};
92///
93/// struct MockTimers(Vec<String>);
94/// impl TimerRuntime for MockTimers {
95/// type TimerId = u8;
96/// fn set_timer(&mut self, id: u8, dur: Duration) {
97/// self.0.push(format!("set({id}, {dur:?})"));
98/// }
99/// fn kill_timer(&mut self, id: u8) {
100/// self.0.push(format!("kill({id})"));
101/// }
102/// }
103///
104/// struct MockExecutor(Vec<i32>);
105/// impl ActionExecutor for MockExecutor {
106/// type Action = i32;
107/// fn execute(&mut self, actions: &[i32]) {
108/// self.0.extend_from_slice(actions);
109/// }
110/// }
111///
112/// let response = Response::emit_one(42)
113/// .with_timer(1, Duration::from_millis(100));
114///
115/// let mut timers = MockTimers(vec![]);
116/// let mut executor = MockExecutor(vec![]);
117/// let consumed = dispatch(&response, &mut timers, &mut executor);
118///
119/// assert!(consumed);
120/// assert_eq!(executor.0, vec![42]);
121/// assert_eq!(timers.0.len(), 1);
122/// ```
123pub fn dispatch<A, T: Copy + Eq + core::fmt::Debug>(
124 response: &Response<A, T>,
125 timers: &mut impl TimerRuntime<TimerId = T>,
126 executor: &mut impl ActionExecutor<Action = A>,
127) -> bool {
128 for cmd in &response.timers {
129 match *cmd {
130 TimerCommand::Set { id, duration } => timers.set_timer(id, duration),
131 TimerCommand::Kill { id } => timers.kill_timer(id),
132 }
133 }
134 if !response.actions.is_empty() {
135 executor.execute(&response.actions);
136 }
137 response.consumed
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 struct RecordTimers(Vec<TimerCommand<u8>>);
145 impl TimerRuntime for RecordTimers {
146 type TimerId = u8;
147 fn set_timer(&mut self, id: u8, duration: Duration) {
148 self.0.push(TimerCommand::Set { id, duration });
149 }
150 fn kill_timer(&mut self, id: u8) {
151 self.0.push(TimerCommand::Kill { id });
152 }
153 }
154
155 struct RecordActions(Vec<&'static str>);
156 impl ActionExecutor for RecordActions {
157 type Action = &'static str;
158 fn execute(&mut self, actions: &[&'static str]) {
159 self.0.extend_from_slice(actions);
160 }
161 }
162
163 #[test]
164 fn dispatch_processes_timers_then_actions() {
165 let response = Response::emit(vec!["a", "b"])
166 .with_timer(1, Duration::from_millis(100))
167 .with_kill_timer(2);
168
169 let mut timers = RecordTimers(vec![]);
170 let mut executor = RecordActions(vec![]);
171 let consumed = dispatch(&response, &mut timers, &mut executor);
172
173 assert!(consumed);
174 assert_eq!(timers.0.len(), 2);
175 assert_eq!(executor.0, vec!["a", "b"]);
176 }
177
178 #[test]
179 fn dispatch_pass_through_returns_false() {
180 let response: Response<&str, u8> = Response::pass_through();
181 let mut timers = RecordTimers(vec![]);
182 let mut executor = RecordActions(vec![]);
183 let consumed = dispatch(&response, &mut timers, &mut executor);
184
185 assert!(!consumed);
186 assert!(timers.0.is_empty());
187 assert!(executor.0.is_empty());
188 }
189
190 #[test]
191 fn dispatch_consume_no_actions() {
192 let response: Response<&str, u8> =
193 Response::consume().with_timer(0, Duration::from_millis(50));
194 let mut timers = RecordTimers(vec![]);
195 let mut executor = RecordActions(vec![]);
196 let consumed = dispatch(&response, &mut timers, &mut executor);
197
198 assert!(consumed);
199 assert_eq!(timers.0.len(), 1);
200 assert!(executor.0.is_empty());
201 }
202}