subx_cli/core/uuidv7.rs
1//! Shared UUIDv7 identifier generation with strict 1ms spacing.
2//!
3//! This generator is used by both the subtitle translation engine (for cue
4//! IDs) and the media discovery layer (for file IDs). Generating UUIDv7 IDs
5//! in batch order means the embedded `unix_time_ts` (the most-significant 48
6//! bits) reflects request order and can be inspected from logs without
7//! consulting external state.
8//!
9//! This module enforces an additional constraint on top of the UUIDv7
10//! algorithm: adjacent ID generations are spaced by at least 1 millisecond,
11//! so each next ID's `unix_time_ts` is strictly greater than the previous
12//! one. This avoids same-millisecond ambiguity that the standard UUIDv7
13//! algorithm normally resolves through random or monotonic counter bits.
14
15use std::thread;
16use std::time::Duration;
17
18use uuid::Uuid;
19
20/// Stateful UUIDv7 ID generator shared by media discovery and translation.
21///
22/// Calling [`Uuidv7Generator::next_id`] sleeps for at least 1ms when the
23/// previous call observed the same millisecond, guaranteeing strictly
24/// increasing `unix_time_ts` values across all IDs produced by a single
25/// generator instance.
26#[derive(Debug, Default)]
27pub struct Uuidv7Generator {
28 last_unix_ts_ms: Option<u64>,
29}
30
31impl Uuidv7Generator {
32 /// Create a fresh generator.
33 pub fn new() -> Self {
34 Self {
35 last_unix_ts_ms: None,
36 }
37 }
38
39 /// Generate the next UUIDv7 ID with strict 1ms spacing.
40 ///
41 /// The implementation extracts the embedded `unix_time_ts` from the
42 /// generated UUIDv7. If the new timestamp is not strictly greater than
43 /// the previous one, the thread sleeps long enough for the next call to
44 /// land in a later millisecond, then retries.
45 pub fn next_id(&mut self) -> Uuid {
46 loop {
47 let id = Uuid::now_v7();
48 let ts = unix_time_ms(&id);
49 match self.last_unix_ts_ms {
50 Some(last) if ts <= last => {
51 let wait = last.saturating_sub(ts).saturating_add(1);
52 thread::sleep(Duration::from_millis(wait));
53 continue;
54 }
55 _ => {
56 self.last_unix_ts_ms = Some(ts);
57 return id;
58 }
59 }
60 }
61 }
62}
63
64/// Generate `count` UUIDv7 IDs in sequence with strict 1ms spacing.
65///
66/// Convenience wrapper around [`Uuidv7Generator`] for callers that just need
67/// a vector of IDs.
68pub fn generate_ids(count: usize) -> Vec<Uuid> {
69 let mut id_gen = Uuidv7Generator::new();
70 let mut ids = Vec::with_capacity(count);
71 for _ in 0..count {
72 ids.push(id_gen.next_id());
73 }
74 ids
75}
76
77/// Extract the 48-bit `unix_time_ts` field from a UUIDv7 value.
78///
79/// Returns the timestamp in milliseconds since the Unix epoch.
80pub fn unix_time_ms(id: &Uuid) -> u64 {
81 let bytes = id.as_bytes();
82 ((bytes[0] as u64) << 40)
83 | ((bytes[1] as u64) << 32)
84 | ((bytes[2] as u64) << 24)
85 | ((bytes[3] as u64) << 16)
86 | ((bytes[4] as u64) << 8)
87 | (bytes[5] as u64)
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn ids_are_uuidv7() {
96 let ids = generate_ids(3);
97 for id in &ids {
98 assert_eq!(id.get_version_num(), 7, "expected UUIDv7, got {}", id);
99 }
100 }
101
102 #[test]
103 fn timestamps_strictly_increase() {
104 let ids = generate_ids(5);
105 let mut last = 0u64;
106 for (i, id) in ids.iter().enumerate() {
107 let ts = unix_time_ms(id);
108 if i > 0 {
109 assert!(
110 ts > last,
111 "expected strictly increasing unix_time_ts, got {} after {}",
112 ts,
113 last
114 );
115 }
116 last = ts;
117 }
118 }
119
120 #[test]
121 fn tight_loop_strictly_monotonic_unix_time_ts() {
122 // Regression: even when next_id is called as fast as possible, the
123 // 1ms-spacing contract guarantees strictly increasing unix_time_ts.
124 let mut generator = Uuidv7Generator::new();
125 let mut last_ts = 0u64;
126 for i in 0..20 {
127 let id = generator.next_id();
128 let ts = unix_time_ms(&id);
129 if i > 0 {
130 assert!(
131 ts > last_ts,
132 "expected strictly increasing unix_time_ts at iter {}, got {} after {}",
133 i,
134 ts,
135 last_ts
136 );
137 }
138 last_ts = ts;
139 }
140 }
141}