Skip to main content

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}