nomad_protocol/extensions/
priority.rs

1//! Priority extension (0x0002)
2//!
3//! Allows marking updates with priority levels so that critical state changes
4//! (e.g., error conditions, user input acknowledgment) can be prioritized over
5//! cosmetic updates (e.g., progress bars, animations).
6//!
7//! Wire format for extension data:
8//! ```text
9//! +0  Supported levels bitmap (1 byte)
10//!     - bit 0: CRITICAL supported
11//!     - bit 1: HIGH supported
12//!     - bit 2: NORMAL supported
13//!     - bit 3: LOW supported
14//!     - bit 4: BACKGROUND supported
15//! +1  Default priority (1 byte, 0-4)
16//! ```
17//!
18//! When attached to a sync message, priority is encoded as a single byte (0-4).
19
20use super::negotiation::{ext_type, Extension};
21
22/// Priority levels for state updates
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
24#[repr(u8)]
25pub enum Priority {
26    /// Must be delivered immediately, may preempt other updates
27    /// Use for: error states, disconnect notices, critical alerts
28    Critical = 0,
29
30    /// Important updates that should be delivered promptly
31    /// Use for: user input acknowledgment, command responses
32    High = 1,
33
34    /// Standard priority for most updates
35    /// Use for: regular state synchronization
36    #[default]
37    Normal = 2,
38
39    /// Can be delayed if bandwidth is constrained
40    /// Use for: progress updates, non-critical status
41    Low = 3,
42
43    /// Lowest priority, deliver when convenient
44    /// Use for: analytics, telemetry, prefetch
45    Background = 4,
46}
47
48impl Priority {
49    /// Convert from wire format byte
50    pub fn from_byte(b: u8) -> Option<Self> {
51        match b {
52            0 => Some(Priority::Critical),
53            1 => Some(Priority::High),
54            2 => Some(Priority::Normal),
55            3 => Some(Priority::Low),
56            4 => Some(Priority::Background),
57            _ => None,
58        }
59    }
60
61    /// Convert to wire format byte
62    pub fn to_byte(self) -> u8 {
63        self as u8
64    }
65
66    /// Get bitmask for this priority level
67    pub fn to_bitmask(self) -> u8 {
68        1 << (self as u8)
69    }
70}
71
72/// Bitmap of supported priority levels
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub struct PrioritySupportBitmap(pub u8);
75
76impl PrioritySupportBitmap {
77    /// All priority levels supported
78    pub const ALL: Self = Self(0b00011111);
79
80    /// Only normal priority (minimal support)
81    pub const NORMAL_ONLY: Self = Self(0b00000100);
82
83    /// Check if a priority level is supported
84    pub fn supports(&self, priority: Priority) -> bool {
85        (self.0 & priority.to_bitmask()) != 0
86    }
87
88    /// Add support for a priority level
89    pub fn add(&mut self, priority: Priority) {
90        self.0 |= priority.to_bitmask();
91    }
92
93    /// Remove support for a priority level
94    pub fn remove(&mut self, priority: Priority) {
95        self.0 &= !priority.to_bitmask();
96    }
97
98    /// Iterate over supported priorities (highest to lowest)
99    pub fn iter_supported(&self) -> impl Iterator<Item = Priority> + '_ {
100        [
101            Priority::Critical,
102            Priority::High,
103            Priority::Normal,
104            Priority::Low,
105            Priority::Background,
106        ]
107        .into_iter()
108        .filter(|p| self.supports(*p))
109    }
110}
111
112/// Priority extension configuration
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct PriorityConfig {
115    /// Bitmap of supported priority levels
116    pub supported: PrioritySupportBitmap,
117    /// Default priority for messages without explicit priority
118    pub default: Priority,
119}
120
121impl Default for PriorityConfig {
122    fn default() -> Self {
123        Self {
124            supported: PrioritySupportBitmap::ALL,
125            default: Priority::Normal,
126        }
127    }
128}
129
130impl PriorityConfig {
131    /// Create config supporting all priorities
132    pub fn all() -> Self {
133        Self::default()
134    }
135
136    /// Create config with only normal priority
137    pub fn minimal() -> Self {
138        Self {
139            supported: PrioritySupportBitmap::NORMAL_ONLY,
140            default: Priority::Normal,
141        }
142    }
143
144    /// Wire size of this config
145    pub const fn wire_size() -> usize {
146        2 // supported bitmap (1) + default (1)
147    }
148
149    /// Encode to extension
150    pub fn to_extension(&self) -> Extension {
151        Extension::new(ext_type::PRIORITY, vec![self.supported.0, self.default.to_byte()])
152    }
153
154    /// Decode from extension
155    pub fn from_extension(ext: &Extension) -> Option<Self> {
156        if ext.ext_type != ext_type::PRIORITY || ext.data.len() < 2 {
157            return None;
158        }
159        let default = Priority::from_byte(ext.data[1])?;
160        Some(Self {
161            supported: PrioritySupportBitmap(ext.data[0]),
162            default,
163        })
164    }
165
166    /// Negotiate between client and server configs
167    ///
168    /// Returns the intersection of supported levels with the lower default priority.
169    pub fn negotiate(client: &Self, server: &Self) -> Self {
170        let supported = PrioritySupportBitmap(client.supported.0 & server.supported.0);
171        let default = client.default.max(server.default); // Higher value = lower priority
172        Self { supported, default }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_priority_ordering() {
182        assert!(Priority::Critical < Priority::High);
183        assert!(Priority::High < Priority::Normal);
184        assert!(Priority::Normal < Priority::Low);
185        assert!(Priority::Low < Priority::Background);
186    }
187
188    #[test]
189    fn test_priority_byte_roundtrip() {
190        for p in [
191            Priority::Critical,
192            Priority::High,
193            Priority::Normal,
194            Priority::Low,
195            Priority::Background,
196        ] {
197            assert_eq!(Priority::from_byte(p.to_byte()), Some(p));
198        }
199        assert_eq!(Priority::from_byte(5), None);
200        assert_eq!(Priority::from_byte(255), None);
201    }
202
203    #[test]
204    fn test_bitmap_operations() {
205        let mut bitmap = PrioritySupportBitmap(0);
206        assert!(!bitmap.supports(Priority::Normal));
207
208        bitmap.add(Priority::Normal);
209        bitmap.add(Priority::High);
210        assert!(bitmap.supports(Priority::Normal));
211        assert!(bitmap.supports(Priority::High));
212        assert!(!bitmap.supports(Priority::Critical));
213
214        bitmap.remove(Priority::Normal);
215        assert!(!bitmap.supports(Priority::Normal));
216        assert!(bitmap.supports(Priority::High));
217    }
218
219    #[test]
220    fn test_bitmap_all() {
221        let bitmap = PrioritySupportBitmap::ALL;
222        assert!(bitmap.supports(Priority::Critical));
223        assert!(bitmap.supports(Priority::High));
224        assert!(bitmap.supports(Priority::Normal));
225        assert!(bitmap.supports(Priority::Low));
226        assert!(bitmap.supports(Priority::Background));
227    }
228
229    #[test]
230    fn test_config_extension_roundtrip() {
231        let config = PriorityConfig {
232            supported: PrioritySupportBitmap::ALL,
233            default: Priority::Low,
234        };
235
236        let ext = config.to_extension();
237        assert_eq!(ext.ext_type, ext_type::PRIORITY);
238
239        let decoded = PriorityConfig::from_extension(&ext).unwrap();
240        assert_eq!(decoded, config);
241    }
242
243    #[test]
244    fn test_negotiate() {
245        let client = PriorityConfig {
246            supported: PrioritySupportBitmap(0b00011111), // All
247            default: Priority::Normal,
248        };
249        let server = PriorityConfig {
250            supported: PrioritySupportBitmap(0b00000111), // Critical, High, Normal only
251            default: Priority::High,
252        };
253
254        let result = PriorityConfig::negotiate(&client, &server);
255
256        // Intersection of supported levels
257        assert!(result.supported.supports(Priority::Critical));
258        assert!(result.supported.supports(Priority::High));
259        assert!(result.supported.supports(Priority::Normal));
260        assert!(!result.supported.supports(Priority::Low));
261        assert!(!result.supported.supports(Priority::Background));
262
263        // Default is the higher value (lower priority) between Normal and High
264        assert_eq!(result.default, Priority::Normal);
265    }
266}