Skip to main content

uqa_client/notifications/
timing.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Exact millisecond validation, separate from clock scheduling and qualified policy selection.
8
9use super::ProtocolError;
10use std::num::NonZeroU64;
11
12/// Supplied by the receiving runtime and its configured deadline policy; the wire protocol invents no timer default.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct TimerLimits {
15    maximum_timer_ms: NonZeroU64,
16    maximum_idle_ms: Option<NonZeroU64>,
17}
18
19impl TimerLimits {
20    pub const fn new(maximum_timer_ms: NonZeroU64, maximum_idle_ms: Option<NonZeroU64>) -> Self {
21        Self {
22            maximum_timer_ms,
23            maximum_idle_ms,
24        }
25    }
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct NotificationTiming {
30    heartbeat_interval: u64,
31    idle_timeout: u64,
32    timing_margin: u64,
33}
34
35impl NotificationTiming {
36    pub fn new(
37        heartbeat_interval_ms: u64,
38        idle_timeout_ms: u64,
39        timing_margin_ms: u64,
40        limits: TimerLimits,
41    ) -> Result<Self, ProtocolError> {
42        if [heartbeat_interval_ms, idle_timeout_ms, timing_margin_ms].contains(&0) {
43            return Err(ProtocolError::Timing);
44        }
45        if [heartbeat_interval_ms, idle_timeout_ms, timing_margin_ms]
46            .into_iter()
47            .any(|value| value > limits.maximum_timer_ms.get())
48            || limits
49                .maximum_idle_ms
50                .is_some_and(|limit| idle_timeout_ms > limit.get())
51        {
52            return Err(ProtocolError::TimerRange);
53        }
54        let minimum = heartbeat_interval_ms
55            .checked_mul(3)
56            .and_then(|value| value.checked_add(timing_margin_ms))
57            .ok_or(ProtocolError::Timing)?;
58        if idle_timeout_ms <= minimum {
59            return Err(ProtocolError::Timing);
60        }
61        Ok(Self {
62            heartbeat_interval: heartbeat_interval_ms,
63            idle_timeout: idle_timeout_ms,
64            timing_margin: timing_margin_ms,
65        })
66    }
67
68    pub const fn heartbeat_interval_ms(self) -> u64 {
69        self.heartbeat_interval
70    }
71    pub const fn idle_timeout_ms(self) -> u64 {
72        self.idle_timeout
73    }
74    pub const fn timing_margin_ms(self) -> u64 {
75        self.timing_margin
76    }
77}
78
79pub(super) fn decimal(value: &str) -> Option<u64> {
80    if value.is_empty()
81        || value.len() > 20
82        || value.starts_with('0')
83        || !value.bytes().all(|byte| byte.is_ascii_digit())
84    {
85        return None;
86    }
87    value.parse().ok()
88}