Skip to main content

asupersync/types/
id.rs

1//! Identifier types for runtime entities.
2//!
3//! These types provide type-safe identifiers for the core runtime entities:
4//! regions, tasks, and obligations. They wrap arena indices with type safety.
5
6use crate::util::ArenaIndex;
7use core::fmt;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use std::ops::Add;
10use std::sync::atomic::{AtomicU32, Ordering};
11use std::time::Duration;
12
13static EPHEMERAL_REGION_COUNTER: AtomicU32 = AtomicU32::new(1);
14static EPHEMERAL_TASK_COUNTER: AtomicU32 = AtomicU32::new(1);
15
16/// A unique identifier for a region in the runtime.
17///
18/// Regions form a tree structure and own all work spawned within them.
19#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct RegionId(pub(crate) ArenaIndex);
21
22impl RegionId {
23    /// Creates a new region ID from an arena index (internal use).
24    #[inline]
25    #[must_use]
26    #[cfg_attr(feature = "test-internals", visibility::make(pub))]
27    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
28        Self(index)
29    }
30
31    /// Returns a 64-bit integer representation of this RegionId.
32    #[inline]
33    #[must_use]
34    pub fn as_u64(&self) -> u64 {
35        ((self.0.generation() as u64) << 32) | (self.0.index() as u64)
36    }
37
38    /// Returns the underlying arena index (internal use).
39    #[inline]
40    #[must_use]
41    #[allow(dead_code)]
42    #[cfg(not(feature = "test-internals"))]
43    pub(crate) const fn arena_index(self) -> ArenaIndex {
44        self.0
45    }
46
47    /// Returns the underlying arena index (internal use).
48    #[inline]
49    #[must_use]
50    #[allow(dead_code)]
51    #[cfg(feature = "test-internals")]
52    pub const fn arena_index(self) -> ArenaIndex {
53        self.0
54    }
55
56    /// Creates a region ID for testing/benchmarking purposes.
57    #[doc(hidden)]
58    #[inline]
59    #[must_use]
60    pub const fn new_for_test(index: u32, generation: u32) -> Self {
61        Self(ArenaIndex::new(index, generation))
62    }
63
64    /// Creates a default region ID for testing purposes.
65    ///
66    /// This creates an ID with index 0 and generation 0, suitable for
67    /// unit tests that don't care about specific ID values.
68    #[doc(hidden)]
69    #[inline]
70    #[must_use]
71    pub const fn testing_default() -> Self {
72        Self(ArenaIndex::new(0, 0))
73    }
74
75    /// Creates a new ephemeral region ID for request-scoped contexts created
76    /// outside the runtime scheduler.
77    ///
78    /// This is intended for production request handling that needs unique
79    /// identifiers without full runtime region registration.
80    #[inline]
81    #[must_use]
82    pub fn new_ephemeral() -> Self {
83        let index = EPHEMERAL_REGION_COUNTER.fetch_add(1, Ordering::Relaxed);
84        Self(ArenaIndex::new(index, 1))
85    }
86}
87
88impl fmt::Debug for RegionId {
89    #[inline]
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "RegionId({}:{})", self.0.index(), self.0.generation())
92    }
93}
94
95impl fmt::Display for RegionId {
96    #[inline]
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "R{}", self.0.index())
99    }
100}
101
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103struct SerdeArenaIndex {
104    index: u32,
105    generation: u32,
106}
107
108impl SerdeArenaIndex {
109    #[inline]
110    const fn to_arena(self) -> ArenaIndex {
111        ArenaIndex::new(self.index, self.generation)
112    }
113}
114
115impl From<ArenaIndex> for SerdeArenaIndex {
116    #[inline]
117    fn from(value: ArenaIndex) -> Self {
118        Self {
119            index: value.index(),
120            generation: value.generation(),
121        }
122    }
123}
124
125impl Serialize for RegionId {
126    #[inline]
127    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
128    where
129        S: Serializer,
130    {
131        SerdeArenaIndex::from(self.0).serialize(serializer)
132    }
133}
134
135impl<'de> Deserialize<'de> for RegionId {
136    #[inline]
137    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138    where
139        D: Deserializer<'de>,
140    {
141        let idx = SerdeArenaIndex::deserialize(deserializer)?;
142        Ok(Self(idx.to_arena()))
143    }
144}
145
146/// A unique identifier for a task in the runtime.
147///
148/// Tasks are units of concurrent execution owned by regions.
149#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
150pub struct TaskId(pub(crate) ArenaIndex);
151
152impl TaskId {
153    /// Creates a new task ID from an arena index (internal use).
154    #[inline]
155    #[must_use]
156    #[allow(dead_code)]
157    #[cfg_attr(feature = "test-internals", visibility::make(pub))]
158    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
159        Self(index)
160    }
161
162    /// Returns a 64-bit integer representation of this `TaskId`.
163    #[inline]
164    #[must_use]
165    pub fn as_u64(&self) -> u64 {
166        ((self.0.generation() as u64) << 32) | (self.0.index() as u64)
167    }
168
169    /// Returns the underlying arena index (internal use).
170    #[inline]
171    #[must_use]
172    #[allow(dead_code)]
173    #[cfg(not(feature = "test-internals"))]
174    pub(crate) const fn arena_index(self) -> ArenaIndex {
175        self.0
176    }
177
178    /// Returns the underlying arena index (internal use).
179    #[inline]
180    #[must_use]
181    #[allow(dead_code)]
182    #[cfg(feature = "test-internals")]
183    pub const fn arena_index(self) -> ArenaIndex {
184        self.0
185    }
186
187    /// Creates a task ID for testing/benchmarking purposes.
188    #[doc(hidden)]
189    #[inline]
190    #[must_use]
191    pub const fn new_for_test(index: u32, generation: u32) -> Self {
192        Self(ArenaIndex::new(index, generation))
193    }
194
195    /// Creates a default task ID for testing purposes.
196    ///
197    /// This creates an ID with index 0 and generation 0, suitable for
198    /// unit tests that don't care about specific ID values.
199    #[doc(hidden)]
200    #[inline]
201    #[must_use]
202    pub const fn testing_default() -> Self {
203        Self(ArenaIndex::new(0, 0))
204    }
205
206    /// Creates a new ephemeral task ID for request-scoped contexts created
207    /// outside the runtime scheduler.
208    #[inline]
209    #[must_use]
210    pub fn new_ephemeral() -> Self {
211        let index = EPHEMERAL_TASK_COUNTER.fetch_add(1, Ordering::Relaxed);
212        Self(ArenaIndex::new(index, 1))
213    }
214}
215
216impl fmt::Debug for TaskId {
217    #[inline]
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        write!(f, "TaskId({}:{})", self.0.index(), self.0.generation())
220    }
221}
222
223impl fmt::Display for TaskId {
224    #[inline]
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(f, "T{}", self.0.index())
227    }
228}
229
230impl Serialize for TaskId {
231    #[inline]
232    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
233    where
234        S: Serializer,
235    {
236        SerdeArenaIndex::from(self.0).serialize(serializer)
237    }
238}
239
240impl<'de> Deserialize<'de> for TaskId {
241    #[inline]
242    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
243    where
244        D: Deserializer<'de>,
245    {
246        let idx = SerdeArenaIndex::deserialize(deserializer)?;
247        Ok(Self(idx.to_arena()))
248    }
249}
250
251/// A unique identifier for an obligation in the runtime.
252///
253/// Obligations represent resources that must be resolved (commit, abort, ack, etc.)
254/// before their owning region can close.
255#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
256pub struct ObligationId(pub(crate) ArenaIndex);
257
258impl ObligationId {
259    /// Creates a new obligation ID from an arena index (internal use).
260    #[inline]
261    #[must_use]
262    #[allow(dead_code)]
263    pub(crate) const fn from_arena(index: ArenaIndex) -> Self {
264        Self(index)
265    }
266
267    /// Returns the underlying arena index (internal use).
268    #[inline]
269    #[must_use]
270    #[allow(dead_code)]
271    #[cfg(not(feature = "test-internals"))]
272    pub(crate) const fn arena_index(self) -> ArenaIndex {
273        self.0
274    }
275
276    /// Returns the underlying arena index (internal use).
277    #[inline]
278    #[must_use]
279    #[allow(dead_code)]
280    #[cfg(feature = "test-internals")]
281    pub const fn arena_index(self) -> ArenaIndex {
282        self.0
283    }
284
285    /// Creates an obligation ID for testing/benchmarking purposes.
286    #[doc(hidden)]
287    #[inline]
288    #[must_use]
289    pub const fn new_for_test(index: u32, generation: u32) -> Self {
290        Self(ArenaIndex::new(index, generation))
291    }
292}
293
294impl fmt::Debug for ObligationId {
295    #[inline]
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        write!(
298            f,
299            "ObligationId({}:{})",
300            self.0.index(),
301            self.0.generation()
302        )
303    }
304}
305
306impl fmt::Display for ObligationId {
307    #[inline]
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        write!(f, "O{}", self.0.index())
310    }
311}
312
313impl Serialize for ObligationId {
314    #[inline]
315    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
316    where
317        S: Serializer,
318    {
319        SerdeArenaIndex::from(self.0).serialize(serializer)
320    }
321}
322
323impl<'de> Deserialize<'de> for ObligationId {
324    #[inline]
325    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
326    where
327        D: Deserializer<'de>,
328    {
329        let idx = SerdeArenaIndex::deserialize(deserializer)?;
330        Ok(Self(idx.to_arena()))
331    }
332}
333
334/// A logical timestamp for the runtime.
335///
336/// In the production runtime, this corresponds to wall-clock time.
337/// In the lab runtime, this is virtual time controlled by the scheduler.
338#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
339pub struct Time(u64);
340
341impl Time {
342    /// The zero instant (epoch).
343    pub const ZERO: Self = Self(0);
344
345    /// The maximum representable instant.
346    pub const MAX: Self = Self(u64::MAX);
347
348    /// Creates a new time from nanoseconds since epoch.
349    #[inline]
350    #[must_use]
351    pub const fn from_nanos(nanos: u64) -> Self {
352        Self(nanos)
353    }
354
355    /// Creates a new time from milliseconds since epoch.
356    #[inline]
357    #[must_use]
358    pub const fn from_millis(millis: u64) -> Self {
359        Self(millis.saturating_mul(1_000_000))
360    }
361
362    /// Creates a new time from seconds since epoch.
363    #[inline]
364    #[must_use]
365    pub const fn from_secs(secs: u64) -> Self {
366        Self(secs.saturating_mul(1_000_000_000))
367    }
368
369    /// Returns the time as nanoseconds since epoch.
370    #[inline]
371    #[must_use]
372    pub const fn as_nanos(self) -> u64 {
373        self.0
374    }
375
376    /// Returns the time as milliseconds since epoch (truncated).
377    #[inline]
378    #[must_use]
379    pub const fn as_millis(self) -> u64 {
380        self.0 / 1_000_000
381    }
382
383    /// Returns the time as seconds since epoch (truncated).
384    #[inline]
385    #[must_use]
386    pub const fn as_secs(self) -> u64 {
387        self.0 / 1_000_000_000
388    }
389
390    /// Adds a duration in nanoseconds, saturating on overflow.
391    #[inline]
392    #[must_use]
393    pub const fn saturating_add_nanos(self, nanos: u64) -> Self {
394        Self(self.0.saturating_add(nanos))
395    }
396
397    /// Subtracts a duration in nanoseconds, saturating at zero.
398    #[inline]
399    #[must_use]
400    pub const fn saturating_sub_nanos(self, nanos: u64) -> Self {
401        Self(self.0.saturating_sub(nanos))
402    }
403
404    /// Returns the duration between two times in nanoseconds.
405    ///
406    /// Returns 0 if `self` is before `earlier` (time travel protection).
407    /// This method uses saturating arithmetic to prevent overflow.
408    #[inline]
409    #[must_use]
410    pub const fn duration_since(self, earlier: Self) -> u64 {
411        self.0.saturating_sub(earlier.0)
412    }
413}
414
415impl Add<Duration> for Time {
416    type Output = Self;
417
418    #[inline]
419    fn add(self, rhs: Duration) -> Self::Output {
420        let nanos: u64 = rhs.as_nanos().min(u128::from(u64::MAX)) as u64;
421        self.saturating_add_nanos(nanos)
422    }
423}
424
425impl fmt::Debug for Time {
426    #[inline]
427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428        write!(f, "Time({}ns)", self.0)
429    }
430}
431
432impl fmt::Display for Time {
433    #[inline]
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        if self.0 >= 1_000_000_000 {
436            write!(
437                f,
438                "{}.{:03}s",
439                self.0 / 1_000_000_000,
440                (self.0 / 1_000_000) % 1000
441            )
442        } else if self.0 >= 1_000_000 {
443            write!(f, "{}ms", self.0 / 1_000_000)
444        } else if self.0 >= 1_000 {
445            write!(f, "{}us", self.0 / 1_000)
446        } else {
447            write!(f, "{}ns", self.0)
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn time_conversions() {
458        assert_eq!(Time::from_secs(1).as_nanos(), 1_000_000_000);
459        assert_eq!(Time::from_millis(1).as_nanos(), 1_000_000);
460        assert_eq!(Time::from_nanos(1).as_nanos(), 1);
461
462        assert_eq!(Time::from_nanos(1_500_000_000).as_secs(), 1);
463        assert_eq!(Time::from_nanos(1_500_000_000).as_millis(), 1500);
464    }
465
466    #[test]
467    fn time_arithmetic() {
468        let t1 = Time::from_secs(1);
469        let t2 = t1.saturating_add_nanos(500_000_000);
470        assert_eq!(t2.as_millis(), 1500);
471
472        let t3 = t2.saturating_sub_nanos(2_000_000_000);
473        assert_eq!(t3, Time::ZERO);
474    }
475
476    #[test]
477    fn time_ordering() {
478        assert!(Time::from_secs(1) < Time::from_secs(2));
479        assert!(Time::from_millis(1000) == Time::from_secs(1));
480    }
481
482    // ---- RegionId ----
483
484    #[test]
485    fn region_id_debug_format() {
486        let id = RegionId::new_for_test(5, 3);
487        let dbg = format!("{id:?}");
488        assert!(dbg.contains("RegionId"), "{dbg}");
489        assert!(dbg.contains('5'), "{dbg}");
490        assert!(dbg.contains('3'), "{dbg}");
491    }
492
493    #[test]
494    fn region_id_display_format() {
495        let id = RegionId::new_for_test(42, 0);
496        assert_eq!(format!("{id}"), "R42");
497    }
498
499    #[test]
500    fn region_id_equality_and_hash() {
501        use crate::util::DetHasher;
502        use std::hash::{Hash, Hasher};
503
504        let a = RegionId::new_for_test(1, 2);
505        let b = RegionId::new_for_test(1, 2);
506        let c = RegionId::new_for_test(1, 3);
507
508        assert_eq!(a, b);
509        assert_ne!(a, c);
510
511        let mut ha = DetHasher::default();
512        let mut hb = DetHasher::default();
513        a.hash(&mut ha);
514        b.hash(&mut hb);
515        assert_eq!(ha.finish(), hb.finish());
516    }
517
518    #[test]
519    fn region_id_ordering() {
520        let a = RegionId::new_for_test(1, 0);
521        let b = RegionId::new_for_test(2, 0);
522        assert!(a < b);
523        assert!(a <= b);
524        assert!(b > a);
525    }
526
527    #[test]
528    fn region_id_copy_clone() {
529        let id = RegionId::new_for_test(1, 0);
530        let copied = id;
531        let cloned = id;
532        assert_eq!(id, copied);
533        assert_eq!(id, cloned);
534    }
535
536    #[test]
537    fn region_id_testing_default() {
538        let id = RegionId::testing_default();
539        assert_eq!(format!("{id}"), "R0");
540    }
541
542    #[test]
543    fn region_id_ephemeral_unique() {
544        let a = RegionId::new_ephemeral();
545        let b = RegionId::new_ephemeral();
546        assert_ne!(a, b);
547    }
548
549    #[test]
550    fn region_id_serde_roundtrip() {
551        let id = RegionId::new_for_test(99, 7);
552        let json = serde_json::to_string(&id).expect("serialize");
553        let deserialized: RegionId = serde_json::from_str(&json).expect("deserialize");
554        assert_eq!(id, deserialized);
555    }
556
557    // ---- TaskId ----
558
559    #[test]
560    fn task_id_debug_format() {
561        let id = TaskId::new_for_test(10, 2);
562        let dbg = format!("{id:?}");
563        assert!(dbg.contains("TaskId"), "{dbg}");
564        assert!(dbg.contains("10"), "{dbg}");
565        assert!(dbg.contains('2'), "{dbg}");
566    }
567
568    #[test]
569    fn task_id_display_format() {
570        let id = TaskId::new_for_test(7, 0);
571        assert_eq!(format!("{id}"), "T7");
572    }
573
574    #[test]
575    fn task_id_equality_and_hash() {
576        use crate::util::DetHasher;
577        use std::hash::{Hash, Hasher};
578
579        let a = TaskId::new_for_test(3, 1);
580        let b = TaskId::new_for_test(3, 1);
581        let c = TaskId::new_for_test(3, 2);
582
583        assert_eq!(a, b);
584        assert_ne!(a, c);
585
586        let mut ha = DetHasher::default();
587        let mut hb = DetHasher::default();
588        a.hash(&mut ha);
589        b.hash(&mut hb);
590        assert_eq!(ha.finish(), hb.finish());
591    }
592
593    #[test]
594    fn task_id_ordering() {
595        let a = TaskId::new_for_test(1, 0);
596        let b = TaskId::new_for_test(2, 0);
597        assert!(a < b);
598    }
599
600    #[test]
601    fn task_id_copy_clone() {
602        let id = TaskId::new_for_test(5, 1);
603        let copied = id;
604        let cloned = id;
605        assert_eq!(id, copied);
606        assert_eq!(id, cloned);
607    }
608
609    #[test]
610    fn task_id_testing_default() {
611        let id = TaskId::testing_default();
612        assert_eq!(format!("{id}"), "T0");
613    }
614
615    #[test]
616    fn task_id_ephemeral_unique() {
617        let a = TaskId::new_ephemeral();
618        let b = TaskId::new_ephemeral();
619        assert_ne!(a, b);
620    }
621
622    #[test]
623    fn task_id_serde_roundtrip() {
624        let id = TaskId::new_for_test(42, 5);
625        let json = serde_json::to_string(&id).expect("serialize");
626        let deserialized: TaskId = serde_json::from_str(&json).expect("deserialize");
627        assert_eq!(id, deserialized);
628    }
629
630    // ---- ObligationId ----
631
632    #[test]
633    fn obligation_id_debug_format() {
634        let id = ObligationId::new_for_test(8, 1);
635        let dbg = format!("{id:?}");
636        assert!(dbg.contains("ObligationId"), "{dbg}");
637        assert!(dbg.contains('8'), "{dbg}");
638    }
639
640    #[test]
641    fn obligation_id_display_format() {
642        let id = ObligationId::new_for_test(3, 0);
643        assert_eq!(format!("{id}"), "O3");
644    }
645
646    #[test]
647    fn obligation_id_equality_and_hash() {
648        use crate::util::DetHasher;
649        use std::hash::{Hash, Hasher};
650
651        let a = ObligationId::new_for_test(1, 1);
652        let b = ObligationId::new_for_test(1, 1);
653        let c = ObligationId::new_for_test(2, 1);
654
655        assert_eq!(a, b);
656        assert_ne!(a, c);
657
658        let mut ha = DetHasher::default();
659        let mut hb = DetHasher::default();
660        a.hash(&mut ha);
661        b.hash(&mut hb);
662        assert_eq!(ha.finish(), hb.finish());
663    }
664
665    #[test]
666    fn obligation_id_ordering() {
667        let a = ObligationId::new_for_test(1, 0);
668        let b = ObligationId::new_for_test(2, 0);
669        assert!(a < b);
670    }
671
672    #[test]
673    fn obligation_id_copy_clone() {
674        let id = ObligationId::new_for_test(1, 0);
675        let copied = id;
676        let cloned = id;
677        assert_eq!(id, copied);
678        assert_eq!(id, cloned);
679    }
680
681    #[test]
682    fn obligation_id_serde_roundtrip() {
683        let id = ObligationId::new_for_test(77, 3);
684        let json = serde_json::to_string(&id).expect("serialize");
685        let deserialized: ObligationId = serde_json::from_str(&json).expect("deserialize");
686        assert_eq!(id, deserialized);
687    }
688
689    // ---- Time Display ----
690
691    #[test]
692    fn time_display_seconds() {
693        let t = Time::from_secs(2);
694        let disp = format!("{t}");
695        assert_eq!(disp, "2.000s");
696    }
697
698    #[test]
699    fn time_display_seconds_with_millis() {
700        let t = Time::from_nanos(1_234_000_000);
701        let disp = format!("{t}");
702        assert_eq!(disp, "1.234s");
703    }
704
705    #[test]
706    fn time_display_milliseconds() {
707        let t = Time::from_millis(500);
708        let disp = format!("{t}");
709        assert_eq!(disp, "500ms");
710    }
711
712    #[test]
713    fn time_display_microseconds() {
714        let t = Time::from_nanos(5_000);
715        let disp = format!("{t}");
716        assert_eq!(disp, "5us");
717    }
718
719    #[test]
720    fn time_display_nanoseconds() {
721        let t = Time::from_nanos(42);
722        let disp = format!("{t}");
723        assert_eq!(disp, "42ns");
724    }
725
726    #[test]
727    fn time_display_zero() {
728        assert_eq!(format!("{}", Time::ZERO), "0ns");
729    }
730
731    // ---- Time edge cases ----
732
733    #[test]
734    fn time_debug_format() {
735        let t = Time::from_nanos(100);
736        let dbg = format!("{t:?}");
737        assert_eq!(dbg, "Time(100ns)");
738    }
739
740    #[test]
741    fn time_default_is_zero() {
742        assert_eq!(Time::default(), Time::ZERO);
743    }
744
745    #[test]
746    fn time_max_constant() {
747        assert_eq!(Time::MAX.as_nanos(), u64::MAX);
748    }
749
750    #[test]
751    fn time_saturating_add_overflow() {
752        let t = Time::MAX;
753        let result = t.saturating_add_nanos(1);
754        assert_eq!(result, Time::MAX);
755    }
756
757    #[test]
758    fn time_saturating_sub_underflow() {
759        let t = Time::ZERO;
760        let result = t.saturating_sub_nanos(100);
761        assert_eq!(result, Time::ZERO);
762    }
763
764    #[test]
765    fn time_duration_since() {
766        let t1 = Time::from_secs(5);
767        let t2 = Time::from_secs(3);
768        assert_eq!(t1.duration_since(t2), 2_000_000_000);
769        assert_eq!(t2.duration_since(t1), 0); // saturates at 0
770    }
771
772    #[test]
773    fn time_add_duration() {
774        let t = Time::from_secs(1);
775        let result = t + Duration::from_millis(500);
776        assert_eq!(result.as_millis(), 1500);
777    }
778
779    #[test]
780    fn time_from_millis_saturation() {
781        let t = Time::from_millis(u64::MAX);
782        // Should saturate, not overflow
783        assert_eq!(t, Time::MAX);
784    }
785
786    #[test]
787    fn time_from_secs_saturation() {
788        let t = Time::from_secs(u64::MAX);
789        assert_eq!(t, Time::MAX);
790    }
791
792    #[test]
793    fn time_serde_roundtrip() {
794        let t = Time::from_nanos(12345);
795        let json = serde_json::to_string(&t).expect("serialize");
796        let deserialized: Time = serde_json::from_str(&json).expect("deserialize");
797        assert_eq!(t, deserialized);
798    }
799
800    #[test]
801    fn time_hash_consistency() {
802        use crate::util::DetHasher;
803        use std::hash::{Hash, Hasher};
804
805        let a = Time::from_secs(1);
806        let b = Time::from_millis(1000);
807        assert_eq!(a, b);
808
809        let mut ha = DetHasher::default();
810        let mut hb = DetHasher::default();
811        a.hash(&mut ha);
812        b.hash(&mut hb);
813        assert_eq!(ha.finish(), hb.finish());
814    }
815}