nexus_id/snowflake.rs
1//! Snowflake-style ID generation with const generic bit layouts.
2//!
3//! # Overview
4//!
5//! `nexus-id` provides high-performance snowflake ID generation with
6//! user-configurable bit layouts via const generics. No heap allocation,
7//! no dependencies, optimized for trading systems where ID generation
8//! is on the critical path.
9//!
10//! The generator is tick-agnostic: callers provide a raw `u64` tick value
11//! (milliseconds, block numbers, logical clocks—whatever fits your domain).
12//!
13//! # Example
14//!
15//! ```rust
16//! use std::time::Instant;
17//! use nexus_id::Snowflake64;
18//!
19//! // Layout: 42 bits tick, 6 bits worker, 16 bits sequence (65k/tick)
20//! type ClOrdId = Snowflake64<42, 6, 16>;
21//!
22//! let epoch = Instant::now();
23//! let mut id_gen = ClOrdId::new(5);
24//!
25//! // In event loop - snap once per iteration
26//! let tick = (Instant::now() - epoch).as_millis() as u64;
27//! let id: u64 = id_gen.next(tick).unwrap();
28//!
29//! // Unpack to inspect
30//! let (tick, worker, seq) = ClOrdId::unpack(id);
31//! assert_eq!(worker, 5);
32//! assert_eq!(seq, 0);
33//! ```
34//!
35//! # Bit Layout
36//!
37//! IDs are packed MSB to LSB as:
38//!
39//! ```text
40//! [timestamp: TS bits][worker: WK bits][sequence: SQ bits]
41//! ```
42//!
43//! Common configurations:
44//!
45//! | TS | WK | SQ | Years @ ms | Workers | IDs/ms |
46//! |----|----|----|------------|---------|--------|
47//! | 41 | 10 | 12 | 69 | 1024 | 4k |
48//! | 42 | 6 | 16 | 139 | 64 | 65k |
49//! | 39 | 9 | 16 | 17 | 512 | 65k |
50//!
51//! # Performance
52//!
53//! The `next()` method is designed for hot-path usage:
54//! - No syscalls (caller provides tick)
55//! - No allocation
56//! - Predictable branches (same-tick case is common)
57//! - ~24-26 cycles p50 (~6ns at 4GHz)
58
59use core::fmt;
60use core::marker::PhantomData;
61
62use crate::snowflake_id::{MixedId32, MixedId64, SnowflakeId32, SnowflakeId64};
63
64/// Integer types usable as snowflake IDs.
65///
66/// Implemented for `u32`, `i32`, `u64`, `i64`.
67///
68/// This trait is sealed and cannot be implemented outside this crate.
69pub trait IdInt: Copy + private::Sealed {
70 /// Number of bits in this integer type.
71 const BITS: u8;
72
73 /// Convert from raw u64 representation.
74 fn from_raw(v: u64) -> Self;
75
76 /// Convert to raw u64 representation.
77 fn to_raw(self) -> u64;
78}
79
80mod private {
81 pub trait Sealed {}
82 impl Sealed for u32 {}
83 impl Sealed for i32 {}
84 impl Sealed for u64 {}
85 impl Sealed for i64 {}
86}
87
88macro_rules! impl_id_int {
89 ($($ty:ty => $bits:expr),* $(,)?) => {
90 $(
91 impl IdInt for $ty {
92 const BITS: u8 = $bits;
93
94 #[inline(always)]
95 fn from_raw(v: u64) -> Self {
96 v as Self
97 }
98
99 #[inline(always)]
100 fn to_raw(self) -> u64 {
101 self as u64
102 }
103 }
104 )*
105 };
106}
107
108impl_id_int!(
109 u32 => 32,
110 i32 => 32,
111 u64 => 64,
112 i64 => 64,
113);
114
115/// Error returned when a sequence counter is exhausted within a single tick.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct SequenceExhausted {
118 /// Tick value when exhaustion occurred.
119 pub tick: u64,
120 /// Maximum sequence value for this generator's layout.
121 pub max_sequence: u64,
122}
123
124impl fmt::Display for SequenceExhausted {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 write!(
127 f,
128 "sequence exhausted at tick {}: generated {} IDs in one tick",
129 self.tick,
130 self.max_sequence.saturating_add(1)
131 )
132 }
133}
134
135#[cfg(feature = "std")]
136impl std::error::Error for SequenceExhausted {}
137
138/// Error returned by [`Snowflake`] generators.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140#[non_exhaustive]
141pub enum SnowflakeError {
142 /// Sequence counter exhausted for this tick value.
143 Exhausted(SequenceExhausted),
144 /// Tick exceeds the timestamp field width.
145 TimestampOverflow {
146 /// The tick value that caused the overflow.
147 tick: u64,
148 /// Maximum valid tick for this generator's timestamp bit width.
149 max: u64,
150 },
151}
152
153impl fmt::Display for SnowflakeError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 SnowflakeError::Exhausted(e) => e.fmt(f),
157 SnowflakeError::TimestampOverflow { tick, max } => {
158 write!(f, "tick {tick} exceeds timestamp maximum {max}")
159 }
160 }
161 }
162}
163
164#[cfg(feature = "std")]
165impl std::error::Error for SnowflakeError {}
166
167impl From<SequenceExhausted> for SnowflakeError {
168 fn from(e: SequenceExhausted) -> Self {
169 SnowflakeError::Exhausted(e)
170 }
171}
172
173/// Error returned when a worker ID exceeds the maximum for a generator's layout.
174///
175/// Produced by [`Snowflake::try_new`]. The panicking [`Snowflake::new`]
176/// constructor turns this into a panic instead.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct WorkerIdError {
179 /// The worker ID that was supplied.
180 pub worker: u64,
181 /// Maximum valid worker ID for this generator's `WK` bit width.
182 pub max: u64,
183}
184
185impl fmt::Display for WorkerIdError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(f, "worker {} exceeds max {}", self.worker, self.max)
188 }
189}
190
191#[cfg(feature = "std")]
192impl std::error::Error for WorkerIdError {}
193
194/// Snowflake ID generator.
195///
196/// # Type Parameters
197/// - `T`: Output integer type (`u32`, `i32`, `u64`, `i64`)
198/// - `TS`: Tick bits (ordering field)
199/// - `WK`: Worker bits (0 for single-worker systems)
200/// - `SQ`: Sequence bits
201///
202/// # Layout (MSB to LSB)
203/// ```text
204/// [tick: TS bits][worker: WK bits][sequence: SQ bits]
205/// ```
206///
207/// # Compile-Time Validation
208/// - `TS + WK + SQ <= T::BITS`
209/// - `TS > 0`
210/// - `SQ > 0`
211///
212/// # Tick
213///
214/// The generator is tick-agnostic. The caller provides a raw `u64` value
215/// to `next()` which becomes the ordering field in the ID. Common choices:
216/// - Milliseconds since an application epoch (`(now - epoch).as_millis() as u64`)
217/// - Block number (blockchain)
218/// - Logical clock / Lamport counter
219///
220/// The only requirement is that the value is monotonically non-decreasing.
221///
222/// # Example
223/// ```rust
224/// use std::time::Instant;
225/// use nexus_id::Snowflake64;
226///
227/// // 42 bits tick, 6 bits worker, 16 bits sequence
228/// type TradingId = Snowflake64<42, 6, 16>;
229///
230/// let epoch = Instant::now();
231/// let mut id_gen = TradingId::new(5);
232///
233/// let tick = (Instant::now() - epoch).as_millis() as u64;
234/// let id: u64 = id_gen.next(tick).unwrap();
235/// ```
236#[derive(Debug, Clone)]
237pub struct Snowflake<T: IdInt, const TS: u8, const WK: u8, const SQ: u8> {
238 worker_shifted: u64,
239 last_tick: u64,
240 sequence: u64,
241 _marker: PhantomData<T>,
242}
243
244impl<T: IdInt, const TS: u8, const WK: u8, const SQ: u8> Snowflake<T, TS, WK, SQ> {
245 /// Compile-time layout validation.
246 const _VALIDATE: () = {
247 assert!(
248 TS as u16 + WK as u16 + SQ as u16 <= T::BITS as u16,
249 "layout exceeds integer bits: TS + WK + SQ > T::BITS"
250 );
251 assert!(TS > 0, "timestamp bits must be > 0");
252 assert!(SQ > 0, "sequence bits must be > 0");
253 };
254
255 /// Shift amount for timestamp field.
256 const TS_SHIFT: u8 = WK + SQ;
257
258 /// Shift amount for worker field.
259 const WK_SHIFT: u8 = SQ;
260
261 /// Maximum sequence value for this layout.
262 pub const SEQUENCE_MAX: u64 = (1u64 << SQ) - 1;
263
264 /// Maximum worker value for this layout.
265 pub const WORKER_MAX: u64 = if WK == 0 { 0 } else { (1u64 << WK) - 1 };
266
267 /// Maximum timestamp value for this layout.
268 pub const TIMESTAMP_MAX: u64 = (1u64 << TS) - 1;
269
270 /// Create a new generator.
271 ///
272 /// # Arguments
273 /// * `worker` - Worker ID, must fit in WK bits
274 ///
275 /// # Panics
276 /// Panics if `worker > WORKER_MAX`. Use [`try_new`](Self::try_new) for a
277 /// non-panicking constructor.
278 ///
279 /// # Example
280 /// ```rust
281 /// use nexus_id::Snowflake64;
282 ///
283 /// type MyId = Snowflake64<42, 6, 16>;
284 ///
285 /// let mut id_gen = MyId::new(5);
286 /// ```
287 pub fn new(worker: u64) -> Self {
288 match Self::try_new(worker) {
289 Ok(generator) => generator,
290 Err(e) => panic!("{e}"),
291 }
292 }
293
294 /// Create a new generator, returning an error if `worker` exceeds the
295 /// maximum for this generator's `WK` bit width.
296 ///
297 /// This is the non-panicking counterpart to [`new`](Self::new).
298 ///
299 /// # Errors
300 /// Returns [`WorkerIdError`] if `worker > WORKER_MAX`.
301 ///
302 /// # Example
303 /// ```rust
304 /// use nexus_id::Snowflake64;
305 ///
306 /// type MyId = Snowflake64<42, 6, 16>;
307 ///
308 /// let id_gen = MyId::try_new(5).unwrap();
309 /// assert!(MyId::try_new(64).is_err()); // 6 bits = max 63
310 /// ```
311 pub fn try_new(worker: u64) -> Result<Self, WorkerIdError> {
312 // Trigger compile-time validation
313 let () = Self::_VALIDATE;
314
315 if worker > Self::WORKER_MAX {
316 return Err(WorkerIdError {
317 worker,
318 max: Self::WORKER_MAX,
319 });
320 }
321
322 Ok(Self {
323 worker_shifted: worker << Self::WK_SHIFT,
324 last_tick: u64::MAX, // Ensures first call takes "new tick" branch
325 sequence: 0,
326 _marker: PhantomData,
327 })
328 }
329
330 /// Generate the next ID.
331 ///
332 /// # Arguments
333 /// * `tick` - Monotonically non-decreasing value for the ordering field.
334 /// Typically `(now - epoch).as_millis() as u64` for time-based IDs, but can
335 /// be any u64 sequence (block numbers, logical clocks, etc.).
336 ///
337 /// # Errors
338 /// Returns [`SequenceExhausted`] if more than `SEQUENCE_MAX` IDs are
339 /// generated with the same tick value. This is a backpressure signal.
340 ///
341 /// # Performance
342 /// This method is designed for hot-path usage:
343 /// - No syscalls (caller provides tick)
344 /// - No allocation
345 /// - Single predictable branch in the common case
346 ///
347 /// # Example
348 /// ```rust
349 /// use std::time::Instant;
350 /// use nexus_id::Snowflake64;
351 ///
352 /// type MyId = Snowflake64<42, 6, 16>;
353 ///
354 /// let epoch = Instant::now();
355 /// let mut id_gen = MyId::new(5);
356 ///
357 /// // Snap time once per event loop iteration
358 /// let tick = (Instant::now() - epoch).as_millis() as u64;
359 ///
360 /// // Generate IDs using that tick
361 /// let id1 = id_gen.next(tick).unwrap();
362 /// let id2 = id_gen.next(tick).unwrap();
363 /// ```
364 pub fn next(&mut self, tick: u64) -> Result<T, SnowflakeError> {
365 if tick > Self::TIMESTAMP_MAX {
366 return Err(SnowflakeError::TimestampOverflow {
367 tick,
368 max: Self::TIMESTAMP_MAX,
369 });
370 }
371 if tick == self.last_tick {
372 self.sequence += 1;
373 if self.sequence > Self::SEQUENCE_MAX {
374 return Err(SnowflakeError::Exhausted(SequenceExhausted {
375 tick,
376 max_sequence: Self::SEQUENCE_MAX,
377 }));
378 }
379 } else {
380 self.last_tick = tick;
381 self.sequence = 0;
382 }
383
384 Ok(T::from_raw(
385 (tick << Self::TS_SHIFT) | self.worker_shifted | self.sequence,
386 ))
387 }
388
389 /// Generate a raw ID with Fibonacci-mixed bits for identity hashers.
390 ///
391 /// Applies a Fibonacci multiply (bijective, ~1 cycle) to the raw ID,
392 /// producing output with uniform bit distribution. Use with identity
393 /// hashers (e.g., `nohash-hasher`) for optimal HashMap performance.
394 ///
395 /// The mixing is NOT reversible from this raw integer — use
396 /// [`next_id()`](Snowflake::next_id) + [`SnowflakeId64::mixed()`] if you
397 /// need to unmix later.
398 ///
399 /// # Example
400 ///
401 /// ```rust
402 /// use nexus_id::Snowflake64;
403 ///
404 /// type OrderId = Snowflake64<42, 6, 16>;
405 ///
406 /// let mut id_gen = OrderId::new(0);
407 ///
408 /// let id = id_gen.mixed(42).unwrap();
409 /// ```
410 pub fn mixed(&mut self, tick: u64) -> Result<T, SnowflakeError> {
411 let raw = self.next(tick)?;
412 Ok(T::from_raw(fibonacci_mix_64(raw.to_raw())))
413 }
414
415 /// Unpack an ID into (timestamp, worker, sequence).
416 ///
417 /// # Example
418 /// ```rust
419 /// use nexus_id::Snowflake64;
420 ///
421 /// type MyId = Snowflake64<42, 6, 16>;
422 ///
423 /// let mut id_gen = MyId::new(5);
424 ///
425 /// let id = id_gen.next(0).unwrap();
426 /// let (ts, worker, seq) = MyId::unpack(id);
427 ///
428 /// assert_eq!(ts, 0);
429 /// assert_eq!(worker, 5);
430 /// assert_eq!(seq, 0);
431 /// ```
432 pub fn unpack(id: T) -> (u64, u64, u64) {
433 let raw = id.to_raw();
434 (
435 raw >> Self::TS_SHIFT,
436 (raw >> Self::WK_SHIFT) & Self::WORKER_MAX,
437 raw & Self::SEQUENCE_MAX,
438 )
439 }
440
441 /// Worker ID for this generator.
442 #[inline]
443 pub const fn worker(&self) -> u64 {
444 self.worker_shifted >> Self::WK_SHIFT
445 }
446
447 /// Current sequence within this millisecond.
448 #[inline]
449 pub const fn sequence(&self) -> u64 {
450 self.sequence
451 }
452
453 /// Last tick value used.
454 #[inline]
455 pub const fn last_tick(&self) -> u64 {
456 self.last_tick
457 }
458}
459
460// Type aliases for convenience
461
462/// 32-bit snowflake generator.
463pub type Snowflake32<const TS: u8, const WK: u8, const SQ: u8> = Snowflake<u32, TS, WK, SQ>;
464
465/// 64-bit snowflake generator.
466pub type Snowflake64<const TS: u8, const WK: u8, const SQ: u8> = Snowflake<u64, TS, WK, SQ>;
467
468/// Signed 32-bit snowflake generator.
469pub type SnowflakeSigned32<const TS: u8, const WK: u8, const SQ: u8> = Snowflake<i32, TS, WK, SQ>;
470
471/// Signed 64-bit snowflake generator.
472pub type SnowflakeSigned64<const TS: u8, const WK: u8, const SQ: u8> = Snowflake<i64, TS, WK, SQ>;
473
474/// Fibonacci multiply for 64-bit hash mixing.
475///
476/// Bijective permutation (~1 cycle). Spreads structured snowflake bits
477/// uniformly across all positions for identity hasher compatibility.
478#[inline(always)]
479const fn fibonacci_mix_64(x: u64) -> u64 {
480 x.wrapping_mul(0x9E37_79B9_7F4A_7C15)
481}
482
483// =============================================================================
484// Typed ID generation (u64)
485// =============================================================================
486
487impl<const TS: u8, const WK: u8, const SQ: u8> Snowflake<u64, TS, WK, SQ> {
488 /// Generate the next ID as a typed [`SnowflakeId64`].
489 ///
490 /// Same as [`next()`](Self::next) but returns the newtype wrapper with
491 /// field extraction and mixing methods.
492 #[inline]
493 pub fn next_id(&mut self, tick: u64) -> Result<SnowflakeId64<TS, WK, SQ>, SnowflakeError> {
494 self.next(tick).map(SnowflakeId64::from_raw)
495 }
496
497 /// Generate a Fibonacci-mixed ID as a typed [`MixedId64`].
498 ///
499 /// The mixed ID can be unmixed back to the original via [`MixedId64::unmix()`].
500 #[inline]
501 pub fn next_mixed(&mut self, tick: u64) -> Result<MixedId64<TS, WK, SQ>, SnowflakeError> {
502 self.next_id(tick).map(|id| id.mixed())
503 }
504}
505
506// =============================================================================
507// Typed ID generation (u32)
508// =============================================================================
509
510impl<const TS: u8, const WK: u8, const SQ: u8> Snowflake<u32, TS, WK, SQ> {
511 /// Generate the next ID as a typed [`SnowflakeId32`].
512 #[inline]
513 pub fn next_id(&mut self, tick: u64) -> Result<SnowflakeId32<TS, WK, SQ>, SnowflakeError> {
514 self.next(tick).map(SnowflakeId32::from_raw)
515 }
516
517 /// Generate a Fibonacci-mixed ID as a typed [`MixedId32`].
518 #[inline]
519 pub fn next_mixed(&mut self, tick: u64) -> Result<MixedId32<TS, WK, SQ>, SnowflakeError> {
520 self.next_id(tick).map(|id| id.mixed())
521 }
522}
523
524#[cfg(all(test, feature = "std"))]
525mod tests {
526 use super::*;
527
528 type TestId = Snowflake64<42, 6, 16>;
529
530 #[test]
531 fn basic_generation() {
532 let mut id_gen = TestId::new(5);
533
534 let id = id_gen.next(0).unwrap();
535 let (ts, worker, seq) = TestId::unpack(id);
536
537 assert_eq!(ts, 0);
538 assert_eq!(worker, 5);
539 assert_eq!(seq, 0);
540 }
541
542 #[test]
543 fn sequence_increments_same_ts() {
544 let mut id_gen = TestId::new(5);
545
546 let id1 = id_gen.next(0).unwrap();
547 let id2 = id_gen.next(0).unwrap();
548 let id3 = id_gen.next(0).unwrap();
549
550 let (_, _, seq1) = TestId::unpack(id1);
551 let (_, _, seq2) = TestId::unpack(id2);
552 let (_, _, seq3) = TestId::unpack(id3);
553
554 assert_eq!(seq1, 0);
555 assert_eq!(seq2, 1);
556 assert_eq!(seq3, 2);
557 }
558
559 #[test]
560 fn sequence_resets_new_ts() {
561 let mut id_gen = TestId::new(5);
562
563 // Generate at timestamp 0
564 let _ = id_gen.next(0).unwrap();
565 let _ = id_gen.next(0).unwrap();
566
567 // Jump to timestamp 1
568 let id = id_gen.next(1).unwrap();
569
570 let (ts, _, seq) = TestId::unpack(id);
571 assert_eq!(ts, 1);
572 assert_eq!(seq, 0);
573 }
574
575 #[test]
576 fn worker_encoded_correctly() {
577 for worker in [0, 1, 31, 63] {
578 let mut id_gen = TestId::new(worker);
579 let id = id_gen.next(0).unwrap();
580 let (_, w, _) = TestId::unpack(id);
581 assert_eq!(w, worker);
582 }
583 }
584
585 #[test]
586 fn ids_are_unique() {
587 let mut id_gen = TestId::new(5);
588
589 let mut ids = Vec::new();
590 for i in 0..1000u64 {
591 // Advance timestamp every 100 IDs
592 let ts = i / 100;
593 ids.push(id_gen.next(ts).unwrap());
594 }
595
596 // Check uniqueness
597 let mut sorted = ids.clone();
598 sorted.sort_unstable();
599 sorted.dedup();
600 assert_eq!(sorted.len(), ids.len());
601 }
602
603 #[test]
604 fn sequence_exhaustion() {
605 // Tiny sequence: 4 bits = max 15
606 type TinySeq = Snowflake64<42, 6, 4>;
607
608 let mut id_gen = TinySeq::new(5);
609
610 // Generate 16 IDs (0-15)
611 for _ in 0..16 {
612 id_gen.next(0).unwrap();
613 }
614
615 // 17th should fail
616 let result = id_gen.next(0);
617 assert!(result.is_err());
618
619 let SnowflakeError::Exhausted(err) = result.unwrap_err() else {
620 panic!("expected Exhausted");
621 };
622 assert_eq!(err.max_sequence, 15);
623 }
624
625 #[test]
626 #[should_panic(expected = "worker 100 exceeds max 63")]
627 fn worker_overflow_panics() {
628 let _id_gen = TestId::new(100); // 6 bits = max 63
629 }
630
631 #[test]
632 fn try_new_rejects_out_of_range_worker() {
633 // 6 worker bits => max 63.
634 assert!(TestId::try_new(63).is_ok());
635 assert_eq!(
636 TestId::try_new(64).unwrap_err(),
637 WorkerIdError {
638 worker: 64,
639 max: 63
640 }
641 );
642 }
643
644 #[test]
645 fn signed_output() {
646 type SignedId = SnowflakeSigned64<42, 6, 16>;
647
648 let mut id_gen = SignedId::new(5);
649
650 let id: i64 = id_gen.next(0).unwrap();
651 let (ts, worker, seq) = SignedId::unpack(id);
652
653 assert_eq!(ts, 0);
654 assert_eq!(worker, 5);
655 assert_eq!(seq, 0);
656 }
657
658 #[test]
659 fn small_layout_32bit() {
660 // 20 bits timestamp, 4 bits worker, 8 bits sequence
661 type SmallId = Snowflake32<20, 4, 8>;
662
663 let mut id_gen = SmallId::new(7);
664
665 let id: u32 = id_gen.next(0).unwrap();
666 let (ts, worker, seq) = SmallId::unpack(id);
667
668 assert_eq!(ts, 0);
669 assert_eq!(worker, 7);
670 assert_eq!(seq, 0);
671 }
672
673 #[test]
674 fn zero_worker_bits() {
675 // Single-worker system: no worker bits
676 type SingleWorker = Snowflake64<48, 0, 16>;
677
678 let mut id_gen = SingleWorker::new(0);
679
680 let id = id_gen.next(0).unwrap();
681 let (ts, worker, seq) = SingleWorker::unpack(id);
682
683 assert_eq!(ts, 0);
684 assert_eq!(worker, 0);
685 assert_eq!(seq, 0);
686 }
687
688 #[test]
689 fn non_time_timestamp() {
690 // Use block numbers as timestamp
691 type BlockId = Snowflake64<32, 8, 24>;
692
693 let mut id_gen = BlockId::new(1);
694
695 let id1 = id_gen.next(1000).unwrap(); // block 1000
696 let id2 = id_gen.next(1000).unwrap(); // same block
697 let id3 = id_gen.next(1001).unwrap(); // next block
698
699 let (ts1, _, seq1) = BlockId::unpack(id1);
700 let (ts2, _, seq2) = BlockId::unpack(id2);
701 let (ts3, _, seq3) = BlockId::unpack(id3);
702
703 assert_eq!(ts1, 1000);
704 assert_eq!(seq1, 0);
705 assert_eq!(ts2, 1000);
706 assert_eq!(seq2, 1);
707 assert_eq!(ts3, 1001);
708 assert_eq!(seq3, 0);
709 }
710
711 #[test]
712 fn timestamp_overflow_is_rejected() {
713 // Snowflake64<2,0,62>: TIMESTAMP_MAX = 3 (2 TS bits).
714 // tick=4 silently truncates to 0 without the check, producing the same
715 // raw ID as tick=0.
716 type Tiny = Snowflake64<2, 0, 62>;
717 let mut g = Tiny::new(0);
718 assert!(g.next(3).is_ok());
719 assert!(matches!(
720 g.next(4),
721 Err(SnowflakeError::TimestampOverflow { tick: 4, max: 3 })
722 ));
723 }
724}