o192 0.2.2

ORION-192: ordered, resilient, independent, URL-safe 192-bit IDs for distributed systems.
Documentation
//! Stateful ORION-192 generator.

use crate::alphabet::{
    DEFAULT_RANDOM_POOL_BYTES, ID_SIZE_BYTES, MAX_COUNTER, MAX_RANDOM_POOL_BYTES, MAX_RELATIVE_MS,
    MIN_RANDOM_POOL_BYTES, NS_PER_MS, RANDOM_SIZE_BYTES,
};
use crate::clock::{create_hybrid_clock, UnixNanoClock};
use crate::codec::{encode_sortable64, write_uint48_be};
use crate::error::OrionIdError;
use crate::parse::{parse, ParsedOrionId};
use crate::random::{default_random_fill, RandomFill, RandomPool};

/// Stateful ORION-192 generator.
///
/// A single instance guarantees **strict local monotonicity**: every
/// emitted ID is lexicographically greater than every previous ID
/// produced by the same instance, regardless of wall-clock movement.
///
/// # Example
///
/// ```
/// use o192::OrionIdGenerator;
///
/// let mut gen = OrionIdGenerator::new();
/// let a = gen.next().unwrap();
/// let b = gen.next().unwrap();
/// assert!(a < b);
/// ```
///
/// # Threading
///
/// `OrionIdGenerator` is `Send` but **not** `Sync`. Use one generator
/// per thread or wrap calls in a `Mutex`.
pub struct OrionIdGenerator {
    epoch_ms: u128,
    /// Last 60-bit logical time key emitted (`None` before the first call).
    last_time_key: Option<u128>,
    counter: u32,
    clock_rollbacks: u64,
    synthetic_ticks: u64,
    clock: UnixNanoClock,
    pool: RandomPool,
    /// Test-only override that short-circuits the hybrid clock and
    /// returns this exact 60-bit logical time key.
    fake_physical_key: Option<u128>,
}

impl OrionIdGenerator {
    /// Create a generator with Unix epoch (`epoch_ms = 0`) and the
    /// default random pool size.
    #[allow(clippy::missing_panics_doc)]
    pub fn new() -> Self {
        Self::with_options(0, DEFAULT_RANDOM_POOL_BYTES)
            .expect("default ORION-192 options must be valid")
    }

    /// Create a generator with a custom millisecond epoch and the
    /// default random pool size.
    pub fn with_epoch_ms(epoch_ms: u128) -> Result<Self, OrionIdError> {
        Self::with_options(epoch_ms, DEFAULT_RANDOM_POOL_BYTES)
    }

    /// Create a generator with explicit epoch and random pool size.
    ///
    /// # Errors
    ///
    /// Returns [`OrionIdError::InvalidOption`] if `random_pool_bytes`
    /// is outside `[14, 16 MiB]`.
    pub fn with_options(epoch_ms: u128, random_pool_bytes: usize) -> Result<Self, OrionIdError> {
        if !(MIN_RANDOM_POOL_BYTES..=MAX_RANDOM_POOL_BYTES).contains(&random_pool_bytes) {
            return Err(OrionIdError::InvalidOption("random_pool_bytes"));
        }

        Ok(Self {
            epoch_ms,
            last_time_key: None,
            counter: 0,
            clock_rollbacks: 0,
            synthetic_ticks: 0,
            clock: create_hybrid_clock(),
            pool: RandomPool::new(random_pool_bytes, default_random_fill),
            fake_physical_key: None,
        })
    }

    /// Builder-style hook for injecting a deterministic CSPRNG. Useful
    /// for tests; production code should keep the default.
    #[doc(hidden)]
    pub fn with_random_fill(mut self, fill: RandomFill) -> Self {
        self.pool = RandomPool::new(DEFAULT_RANDOM_POOL_BYTES, fill);
        self
    }

    /// Configured epoch in milliseconds.
    pub const fn epoch_ms(&self) -> u128 {
        self.epoch_ms
    }

    /// Last logical 60-bit time key, or `None` before the first call.
    pub const fn last_time_key(&self) -> Option<u128> {
        self.last_time_key
    }

    /// Current 20-bit counter value.
    pub const fn counter(&self) -> u32 {
        self.counter
    }

    /// Number of observed backward clock jumps.
    pub const fn clock_rollbacks(&self) -> u64 {
        self.clock_rollbacks
    }

    /// Number of synthesised 1/4096-ms ticks due to counter overflow.
    pub const fn synthetic_ticks(&self) -> u64 {
        self.synthetic_ticks
    }

    /// Generate a new 24-byte ORION-192 identifier.
    pub fn next_bytes(&mut self) -> Result<[u8; ID_SIZE_BYTES], OrionIdError> {
        let physical = self.physical_time_key()?;

        let logical = match self.last_time_key {
            Some(last) if physical <= last => {
                if physical < last {
                    self.clock_rollbacks = self.clock_rollbacks.saturating_add(1);
                }
                self.counter = self.counter.saturating_add(1);
                if self.counter > MAX_COUNTER {
                    let advanced = last.checked_add(1).ok_or(OrionIdError::TimestampOverflow)?;
                    self.counter = 0;
                    self.synthetic_ticks = self.synthetic_ticks.saturating_add(1);
                    advanced
                } else {
                    last
                }
            }
            _ => {
                self.counter = self.pool.next_10_bits()?;
                physical
            }
        };

        self.last_time_key = Some(logical);

        let ms = logical >> 12;
        let fraction = (logical & 0xfff) as u16;
        let counter = self.counter;
        let mut out = [0u8; ID_SIZE_BYTES];

        write_uint48_be(&mut out, 0, ms)?;
        out[6] = ((fraction >> 4) & 0xff) as u8;
        out[7] = (((fraction & 0x0f) << 4) as u8) | (((counter >> 16) & 0x0f) as u8);
        out[8] = ((counter >> 8) & 0xff) as u8;
        out[9] = (counter & 0xff) as u8;
        self.pool
            .fill_into(&mut out[10..(10 + RANDOM_SIZE_BYTES)])?;

        Ok(out)
    }

    /// Generate a new canonical 32-character ORION-192 string.
    #[allow(clippy::should_implement_trait)]
    pub fn next(&mut self) -> Result<String, OrionIdError> {
        encode_sortable64(&self.next_bytes()?)
    }

    /// Parse an ORION-192 string using this generator's epoch.
    pub fn parse(&self, id: &str) -> Result<ParsedOrionId, OrionIdError> {
        parse(id, self.epoch_ms)
    }

    /// Force the next physical time key to a specific value.
    /// **Test-only**: production code should rely on the hybrid clock.
    #[doc(hidden)]
    pub fn set_physical_time_key_for_test(&mut self, key: u128) {
        self.fake_physical_key = Some(key);
    }

    /// Compute the current 60-bit physical time key.
    fn physical_time_key(&mut self) -> Result<u128, OrionIdError> {
        if let Some(fake) = self.fake_physical_key {
            return Ok(fake);
        }

        let epoch_ns = self.epoch_ms.saturating_mul(NS_PER_MS);
        let rel_ns = (self.clock)().saturating_sub(epoch_ns);
        let ms = rel_ns / NS_PER_MS;
        if ms > MAX_RELATIVE_MS {
            return Err(OrionIdError::TimestampOverflow);
        }

        let ns_inside_ms = rel_ns % NS_PER_MS;
        let fraction = (ns_inside_ms * 4096) / NS_PER_MS;
        Ok((ms << 12) | fraction)
    }
}

impl Default for OrionIdGenerator {
    fn default() -> Self {
        Self::new()
    }
}

impl core::fmt::Debug for OrionIdGenerator {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("OrionIdGenerator")
            .field("epoch_ms", &self.epoch_ms)
            .field("last_time_key", &self.last_time_key)
            .field("counter", &self.counter)
            .field("clock_rollbacks", &self.clock_rollbacks)
            .field("synthetic_ticks", &self.synthetic_ticks)
            .finish_non_exhaustive()
    }
}