jacquardSnapshot

← snapshot

2573 bytes
//! The clock this surface commits against.
//!
//! Normally wall time. But a snapshot's address has its timestamp hashed into
//! it — see `jac_object::encode::snapshot_id` — so replaying a stored history
//! against wall time produces different addresses for the same work. That is
//! not a cosmetic difference: it means the ids written to disk describe
//! something the engine can no longer reproduce, which makes them decorative.
//!
//! So loading a repo drives this clock from the recorded timestamps instead.
//! Every snapshot is then committed at the instant it actually happened, the
//! addresses come out identical, and `store::load` can *prove* the round trip
//! rather than assert it.
//!
//! Once a replay runs out of scripted instants it falls back to wall time,
//! which is what any write after the load should get.

use std::collections::VecDeque;
use std::sync::Mutex;

use jac_core::{Clock, SystemClock, Timestamp};

/// Wall time, or a scripted sequence for replaying a stored history.
/// A mutex rather than a cell: this lives inside the shared application state,
/// which axum requires to be `Sync`. It is held for a pop and nothing else —
/// never across an await.
#[derive(Debug, Default)]
pub struct HostClock {
    /// Instants to hand out, oldest first. Empty means wall time.
    scripted: Mutex<VecDeque<i64>>,
}

impl HostClock {
    /// Wall time.
    #[must_use]
    pub const fn system() -> Self {
        Self {
            scripted: Mutex::new(VecDeque::new()),
        }
    }

    /// Hands out `instants` in order, then falls back to wall time.
    #[must_use]
    pub fn replaying(instants: impl IntoIterator<Item = i64>) -> Self {
        Self {
            scripted: Mutex::new(instants.into_iter().collect()),
        }
    }

    /// How many scripted instants remain — a replay that ends early left
    /// snapshots to be stamped with wall time, which is worth noticing.
    #[must_use]
    pub fn remaining(&self) -> usize {
        self.scripted.lock().map_or(0, |q| q.len())
    }
}

impl Clock for HostClock {
    fn now(&self) -> Timestamp {
        // A poisoned lock falls back to wall time rather than panicking: a
        // clock that brings the surface down over bookkeeping is worse than a
        // clock that is briefly wrong.
        self.scripted.lock().map_or_else(
            |_| SystemClock.now(),
            |mut queue| {
                queue
                    .pop_front()
                    .map_or_else(|| SystemClock.now(), Timestamp::from_millis)
            },
        )
    }
}