flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/contracts/src/ids.rs

79 lines2,878 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server, Rust identity service, registration, site redesign1use std::cell::Cell;
2
3const ALPHABET: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz";
4
5thread_local! {
6 /// The millisecond and counter of the last id made, to keep ids made in
7 /// the same millisecond in order.
8 static LAST: Cell<(u64, u16)> = const { Cell::new((0, 0)) };
9}
10
11/// A new id in [TypeID](https://github.com/jetify-com/typeid) format: a type
12/// prefix, then a UUIDv7 in lowercase Crockford base32, such as
13/// `att_01jb2k7x9hfq0b3zj0f5s2m8ra`.
14///
15/// - The prefix says what the id refers to, so ids cannot be mixed up.
16/// - Sorting ids as strings sorts them by creation time, which also keeps
17/// inserts at the end of the primary-key index.
18/// - The suffix decodes to a standard UUIDv7 for systems that want one.
19///
20/// Ids made in the same millisecond by one process increase monotonically:
21/// the UUID's 12-bit `rand_a` field is used as a counter.
22pub fn new_id(prefix: &str, now_ms: u64) -> String {
23 let mut bytes = [0u8; 16];
24 getrandom::getrandom(&mut bytes).expect("no source of randomness");
25
26 let counter = LAST.with(|last| {
27 let (last_ms, last_counter) = last.get();
28 let counter = if now_ms == last_ms {
29 last_counter.wrapping_add(1) & 0x0fff
30 } else {
31 // Start in the lower half so there is room to count up.
32 u16::from_be_bytes([bytes[6], bytes[7]]) & 0x07ff
33 };
34 last.set((now_ms, counter));
35 counter
36 });
37
38 bytes[..6].copy_from_slice(&now_ms.to_be_bytes()[2..]);
39 bytes[6] = 0x70 | (counter >> 8) as u8; // version 7
40 bytes[7] = counter as u8;
41 bytes[8] = 0x80 | (bytes[8] & 0x3f); // RFC 9562 variant
42
43 let value = u128::from_be_bytes(bytes);
44 let mut id = String::with_capacity(prefix.len() + 27);
45 id.push_str(prefix);
46 id.push('_');
47 // 128 bits in 26 characters of 5 bits; the first carries only 3.
48 for index in 0..26 {
49 let shift = 125 - 5 * index;
50 id.push(ALPHABET[((value >> shift) & 31) as usize] as char);
51 }
52 id
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn has_prefix_and_26_character_suffix() {
61 let id = new_id("att", 1_790_000_000_000);
62 let (prefix, suffix) = id.split_once('_').unwrap();
63 assert_eq!(prefix, "att");
64 assert_eq!(suffix.len(), 26);
65 assert!(suffix.bytes().all(|byte| ALPHABET.contains(&byte)));
66 // A 128-bit value never needs more than 3 bits in the first character.
67 assert!(suffix.as_bytes()[0] <= b'7');
68 }
69
70 #[test]
71 fn sorts_by_time_then_by_order_made() {
72 let earlier = new_id("evt", 1_790_000_000_000);
73 let later = new_id("evt", 1_790_000_000_001);
74 assert!(earlier < later);
75
76 let same_ms: Vec<String> = (0..100).map(|_| new_id("evt", 1_790_000_000_002)).collect();
77 assert!(same_ms.windows(2).all(|pair| pair[0] < pair[1]));
78 }
79}