use crate::history::{
clock::ClockProvenance,
epoch::{CohortTopology, EpochTag, LsnKind, MaterializationSafety},
identity::{ArtifactRef, IdentityDiscipline},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp {
pub secs: i64,
pub nanos: u32,
}
impl Timestamp {
#[must_use]
pub fn from_secs(secs: i64) -> Self {
Self { secs, nanos: 0 }
}
}
#[derive(Debug)]
pub struct TemporalState<H> {
pub epoch: EpochTag,
pub ordering_key: Option<LsnKind>,
pub wall_time: Option<Timestamp>,
pub clock: ClockProvenance,
pub safety: MaterializationSafety,
pub handle: H,
}
#[derive(Debug)]
pub struct TemporalCohort<H> {
pub artifact: ArtifactRef,
pub discipline: IdentityDiscipline,
pub topology: CohortTopology,
pub states: Vec<TemporalState<H>>,
}
impl<H> TemporalCohort<H> {
#[must_use]
pub fn at(&self, t: Timestamp) -> Option<&TemporalState<H>> {
self.states
.iter()
.filter(|s| s.wall_time.is_some_and(|wt| wt <= t))
.max_by_key(|s| s.wall_time)
}
#[must_use]
pub fn nearest(&self, t: Timestamp) -> Option<&TemporalState<H>> {
self.states
.iter()
.filter(|s| s.wall_time.is_some())
.min_by_key(|s| {
let wt = s.wall_time.unwrap_or(t);
(wt.secs - t.secs).unsigned_abs()
})
}
pub fn epochs(&self) -> impl Iterator<Item = EpochTag> + '_ {
self.states.iter().map(|s| s.epoch)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tombstone {
pub last_seen_epoch: EpochTag,
pub first_absent_epoch: EpochTag,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentityDiscontinuity {
pub at_epoch: EpochTag,
pub discipline: IdentityDiscipline,
pub description: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::history::clock::{ClockSource, TamperResistance, TrustGrade};
fn clock() -> ClockProvenance {
ClockProvenance {
source: ClockSource::SequenceOnly,
trust_grade: TrustGrade::OrderingOnly,
tamper_resistance: TamperResistance::UserWritable,
ordering_only: false,
skew_known: None,
authenticated: None,
}
}
fn state(handle: u32, secs: Option<i64>) -> TemporalState<u32> {
TemporalState {
epoch: EpochTag([handle as u8; 32]),
ordering_key: None,
wall_time: secs.map(Timestamp::from_secs),
clock: clock(),
safety: MaterializationSafety::ReadOnlySafe,
handle,
}
}
fn cohort(states: Vec<TemporalState<u32>>) -> TemporalCohort<u32> {
TemporalCohort {
artifact: ArtifactRef { claims: vec![] },
discipline: IdentityDiscipline::PathStable,
topology: CohortTopology::DiscreteSet,
states,
}
}
#[test]
fn from_secs_zeroes_nanos() {
let t = Timestamp::from_secs(1_700_000_000);
assert_eq!(t.secs, 1_700_000_000);
assert_eq!(t.nanos, 0);
}
#[test]
fn at_selects_latest_state_at_or_before_t() {
let c = cohort(vec![
state(1, Some(100)),
state(2, Some(200)),
state(3, Some(300)),
state(9, None),
]);
assert_eq!(c.at(Timestamp::from_secs(250)).unwrap().handle, 2);
assert_eq!(c.at(Timestamp::from_secs(300)).unwrap().handle, 3);
assert!(c.at(Timestamp::from_secs(50)).is_none());
}
#[test]
fn at_returns_none_when_no_state_has_wall_time() {
let c = cohort(vec![state(1, None)]);
assert!(c.at(Timestamp::from_secs(100)).is_none());
}
#[test]
fn nearest_picks_closest_and_breaks_ties_toward_first() {
let c = cohort(vec![
state(1, Some(100)),
state(2, Some(200)),
state(3, Some(300)),
state(9, None),
]);
assert_eq!(c.nearest(Timestamp::from_secs(280)).unwrap().handle, 3);
assert_eq!(c.nearest(Timestamp::from_secs(250)).unwrap().handle, 2);
assert_eq!(c.nearest(Timestamp::from_secs(0)).unwrap().handle, 1);
}
#[test]
fn nearest_returns_none_without_wall_times() {
let c = cohort(vec![state(1, None)]);
assert!(c.nearest(Timestamp::from_secs(100)).is_none());
}
#[test]
fn epochs_yields_epoch_tags_in_state_order() {
let c = cohort(vec![state(1, Some(100)), state(2, Some(200))]);
let epochs: Vec<EpochTag> = c.epochs().collect();
assert_eq!(epochs, vec![EpochTag([1u8; 32]), EpochTag([2u8; 32])]);
}
}