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};
pub struct OrionIdGenerator {
epoch_ms: u128,
last_time_key: Option<u128>,
counter: u32,
clock_rollbacks: u64,
synthetic_ticks: u64,
clock: UnixNanoClock,
pool: RandomPool,
fake_physical_key: Option<u128>,
}
impl OrionIdGenerator {
#[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")
}
pub fn with_epoch_ms(epoch_ms: u128) -> Result<Self, OrionIdError> {
Self::with_options(epoch_ms, DEFAULT_RANDOM_POOL_BYTES)
}
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,
})
}
#[doc(hidden)]
pub fn with_random_fill(mut self, fill: RandomFill) -> Self {
self.pool = RandomPool::new(DEFAULT_RANDOM_POOL_BYTES, fill);
self
}
pub const fn epoch_ms(&self) -> u128 {
self.epoch_ms
}
pub const fn last_time_key(&self) -> Option<u128> {
self.last_time_key
}
pub const fn counter(&self) -> u32 {
self.counter
}
pub const fn clock_rollbacks(&self) -> u64 {
self.clock_rollbacks
}
pub const fn synthetic_ticks(&self) -> u64 {
self.synthetic_ticks
}
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)
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<String, OrionIdError> {
encode_sortable64(&self.next_bytes()?)
}
pub fn parse(&self, id: &str) -> Result<ParsedOrionId, OrionIdError> {
parse(id, self.epoch_ms)
}
#[doc(hidden)]
pub fn set_physical_time_key_for_test(&mut self, key: u128) {
self.fake_physical_key = Some(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()
}
}