hyperclock/components/
watcher.rs

1//! Defines watchers that react to the event stream to produce higher-level events.
2
3use crate::common::PhaseId;
4use crate::config::GongConfig;
5use crate::events::GongEvent;
6use crate::time::TickEvent;
7use chrono::Utc;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::broadcast;
11
12/// A function closure that represents a condition to be checked.
13pub type ConditionCheck = Box<dyn Fn() -> bool + Send + Sync>;
14
15/// Watches a phase stream and executes logic at a regular interval.
16#[doc(hidden)]
17pub(crate) struct IntervalWatcher {
18    pub phase_to_watch: PhaseId,
19    pub interval: Duration,
20    pub last_fired: Instant,
21    pub task_logic: Box<dyn FnMut() + Send + Sync>,
22}
23
24impl IntervalWatcher {
25    /// Creates a new `IntervalWatcher`.
26    pub(crate) fn new(
27        phase_to_watch: PhaseId,
28        interval: Duration,
29        task_logic: Box<dyn FnMut() + Send + Sync>,
30    ) -> Self {
31        Self {
32            phase_to_watch,
33            interval,
34            last_fired: Instant::now(),
35            task_logic,
36        }
37    }
38
39    /// Processes a phase event and executes its internal logic if the interval has elapsed.
40    /// Returns `true` if the watcher's logic was executed.
41    pub(crate) fn process_phase(&mut self, current_phase: PhaseId) -> bool {
42        if current_phase == self.phase_to_watch {
43            if self.last_fired.elapsed() >= self.interval {
44                (self.task_logic)();
45                self.last_fired = Instant::now();
46                return true;
47            }
48        }
49        false
50    }
51}
52
53/// Watches the clock for significant, human-meaningful calendar events.
54#[doc(hidden)]
55pub(crate) struct GongWatcher {
56    config: Arc<GongConfig>,
57    last_known_date: chrono::NaiveDate,
58}
59
60impl GongWatcher {
61    /// Creates a new `GongWatcher`.
62    pub(crate) fn new(config: Arc<GongConfig>) -> Self {
63        let now = Utc::now().with_timezone(&config.timezone);
64        Self {
65            config,
66            last_known_date: now.date_naive(),
67        }
68    }
69
70    /// Processes a tick and fires `GongEvent`s if calendar milestones have been crossed.
71    pub(crate) fn process_tick(
72        &mut self,
73        _tick: &TickEvent,
74        gong_event_sender: &broadcast::Sender<GongEvent>,
75    ) {
76        let now = Utc::now().with_timezone(&self.config.timezone);
77        let current_date = now.date_naive();
78
79        if current_date != self.last_known_date {
80            gong_event_sender
81                .send(GongEvent::DateChanged {
82                    new_date: current_date,
83                })
84                .ok();
85
86            for holiday in &self.config.holidays {
87                if holiday.date == current_date {
88                    gong_event_sender
89                        .send(GongEvent::Holiday {
90                            name: holiday.name.clone(),
91                            date: holiday.date,
92                        })
93                        .ok();
94                }
95            }
96            self.last_known_date = current_date;
97        }
98    }
99}
100
101/// Watches for a specific condition to become true.
102#[doc(hidden)]
103pub(crate) struct ConditionalWatcher {
104    pub condition: ConditionCheck,
105    pub task_logic: Box<dyn FnMut() + Send + Sync>,
106    pub is_one_shot: bool,
107}
108
109impl ConditionalWatcher {
110    /// Creates a new `ConditionalWatcher`.
111    pub(crate) fn new(
112        condition: ConditionCheck,
113        task_logic: Box<dyn FnMut() + Send + Sync>,
114        is_one_shot: bool,
115    ) -> Self {
116        Self {
117            condition,
118            task_logic,
119            is_one_shot,
120        }
121    }
122
123    /// Executes the condition check and, if true, executes its internal logic.
124    /// Returns `true` if the condition was met.
125    pub(crate) fn check_and_fire(&mut self) -> bool {
126        if (self.condition)() {
127            (self.task_logic)();
128            true
129        } else {
130            false
131        }
132    }
133}