Skip to main content

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}